제공 : 한빛 네트워크
저자 : 최흥배
이전기사 : 이전 회에는 STL 알고리즘들 중 비슷한 성격의 알고리즘을 모아서 '변경 불가 시퀀스 알고리즘', '변경 가능 시퀀스 알고리즘', '정렬 관련 알고리즘', '범용 수치 알고리즘'으로 크게 네 개로 나누고 이중 '변경 불가 시퀀스 알고리즘' 과 '변경 가능 시퀀스 알고리즘'에 있는 주요 알고리즘의 특징과 사용 방법에 대해서 설명하였습니다.

이번 회는 이전 회에 설명하지 못한 '정렬 관련 알고리즘' 과 '범용 수치 알고리즘'의 주요 알고리즘의 특징과 사용 방법에 대해서 설명하겠습니다.

10.1. 정렬 관련 알고리즘

10.1.1 sort

sort는 컨테이너에 있는 데이터들을 내림차순 또는 오름차순으로 정렬 할 때 가장 자주 사용하는 알고리즘입니다. 컨테이너에 저장하는 데이터의 자료 형이 기본형이라면 STL에 있는 greate나 less 비교 조건자를 사용합니다(STL의 string의 정렬에도 사용할 수 있습니다. 다만 이 때는 알파벳 순서로 정렬합니다). 기본형이 아닌 경우에는 직접 비교 조건자를 만들어서 사용해야 합니다.

sort의 원형
template<class RandomAccessIterator>
   void sort( RandomAccessIterator _First, RandomAccessIterator _Last );
첫 번째와 두 번째 파라미터는 정렬하려는 구간의 시작과 마지막을 가리키는 반복자입니다. 비교 조건자를 필요로 하지 않고 기본형을 저장한 컨테이너를 오름차순으로 정렬합니다.
template<class RandomAccessIterator, class Pr>
   void sort( RandomAccessIterator _First, RandomAccessIterator _Last, BinaryPredicate _Comp );
첫 번째와 두 번째 파라미터는 정렬하려는 구간의 시작과 마지막을 가리키는 반복자입니다. 세 번째 파라미터는 정렬 방법을 기술한 비교 조건자입니다.

sort 알고리즘의 원형을 보면 알 수 있듯이 램덤 접근 반복자를 지원하는 컨테이너만 sort 알고리즘을 사용할 수 있습니다.

sort 사용 방법
vector<int> vec1;
…..
// 오름차순 정렬
sort( vec1.begin(), vec1.end() );

// 내림차순 정렬
Sort( vec1.begin(), vec1.end(), greater<int>() );
< 리스트 1. less와 greater 비교 조건자를 사용한 sort >
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

using namespace std;

int main()
{
  vector<int> vec1(10);
  vector<int> vec2(10);
  vector<int> vec3(10);
  vector <int>::iterator Iter1;

  generate( vec1.begin(), vec1.end(), rand );
  generate( vec2.begin(), vec2.end(), rand );
  generate( vec3.begin(), vec3.end(), rand );

  // 오름차순 정렬
  cout << "vec1 정렬 하기 전" << endl;
  for( Iter1 = vec1.begin(); Iter1 != vec1.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  sort( vec1.begin(), vec1.end() );
  
  cout << "vec1 오름차순 정렬" << endl;
    for( Iter1 = vec1.begin(); Iter1 != vec1.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;
  cout << endl;

  // 내림차순 정렬
  cout << "vec2 정렬 하기 전" << endl;
  for( Iter1 = vec2.begin(); Iter1 != vec2.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;
  
  sort( vec2.begin(), vec2.end(), greater<int>() );

  cout << "vec2 내림차순 정렬" << endl;
    for( Iter1 = vec2.begin(); Iter1 != vec2.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;
  cout << endl;

  // 일부만 내림차순 정렬
  cout << "vec3 정렬 하기 전" << endl;
  for( Iter1 = vec3.begin(); Iter1 != vec3.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  sort( vec3.begin() + 5, vec3.end(), greater<int>() );

  cout << "vec3 일부만 내림차순 정렬" << endl;
    for( Iter1 = vec3.begin(); Iter1 != vec3.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  return 0;
}
< 결과 >

그림1

<리스트 1>은 기본 형 데이터를 정렬하는 예제로 이번에는 유저 정의 형 데이터를 정렬하는 예제를 보여 드리겠습니다.

< 리스트 2. USER 구조체의 Money를 기준으로 정렬 >
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

using namespace std;

struct USER
{
  int UID;
  int Level;
  int Money;
};

struct USER_MONEY_COMP
{
  bool operator()(onst USER& user1, const USER& user2)
  {
    return user1.Money > user2.Money;
  }
};

int main()
{
  USER User1; User1.UID = 1;  User1.Money = 2000;
  USER User2; User2.UID = 2;  User2.Money = 2050;
  USER User3; User3.UID = 3;  User3.Money = 2200;
  USER User4; User4.UID = 4;  User4.Money = 1000;
  USER User5; User5.UID = 5;  User5.Money = 2030;

  vector<USER> Users;
  Users.push_back( User1 );  Users.push_back( User2 );
  Users.push_back( User3 );  Users.push_back( User4 );
  Users.push_back( User5 );

  vector <USER>::iterator Iter1;

  cout << "돈을 기준으로 정렬 하기 전" << endl;
  for( Iter1 = Users.begin(); Iter1 != Users.end(); ++Iter1 ) {
    cout << Iter1->UID << " : " << Iter1->Money << ", ";
  }
  cout << endl << endl;

  sort( Users.begin(), Users.end(), USER_MONEY_COMP() );

  cout << "돈을 기준으로 내림차순으로 정렬" << endl;
  for( Iter1 = Users.begin(); Iter1 != Users.end(); ++Iter1 ) {
    cout << Iter1->UID << " : " << Iter1->Money << ", ";
  }
  cout << endl << endl;

  return 0;
}
< 결과 >

그림2

10.1.2. binary_search

이미 정렬 되어 있는 것 중에서 특정 데이터가 지정한 구간에 있는지 조사하는 알고리즘입니다. 이것도 sort와 같이 비교 조건자가 필요 없는 버전과 필요한 버전 두 개가 있습니다(단 sort와 다르게 랜덤 접근 반복자가 없는 컨테이너도 사용할 수 있습니다).

binary_search 원형
template<class ForwardIterator, class Type>
   bool binary_search( ForwardIterator _First, ForwardIterator _Last, const Type& _Val );

template<class ForwardIterator, class Type, class BinaryPredicate>
   bool binary_search( ForwardIterator _First, ForwardIterator _Last, const Type& _Val, 
                        BinaryPredicate _Comp );
binary_search 사용 방법
vector<int> vec1;
…..
sort( vec1.beign(), vec1.end() );
…...
bool bFind = binary_search( vec1.begin(), vec1.end(), 10 );
binary_search는 정렬한 이후에 사용해야 한다고 앞서 이야기 했습니다. 만약 정렬하지 않고 사용하면 어떻게 될까요?

< 리스트 3. 정렬하지 않고 binary_search 사용 >
int main()
{
  vector<int> vec1;
  vec1.push_back(10);  vec1.push_back(20);  vec1.push_back(15);
  vec1.push_back(7);  vec1.push_back(100); vec1.push_back(40);
  vec1.push_back(11);  vec1.push_back(60);  vec1.push_back(140);

  bool bFind = binary_search( vec1.begin(), vec1.begin() + 5, 15 );
  if( false == bFind ) {
    cout << "15를 찾지 못했습니다." << endl;
  } else {
    cout << "15를 찾았습니다." << endl;
  }

  return 0;
}
디버그 모드로 빌드 후 실행 하면 아래와 같은 에러 창이 나옵니다.

그림3

에러 내용은 시퀀스가 정렬되지 않았다고 나옵니다. 릴리즈 모드 빌드 후 실행하면 위와 같은 에러는 나오지 않지만 결과는 false가 나옵니다.

binary_search를 사용할 때는 꼭 먼저 정렬해야 한다는 것을 잊지 말기를 바랍니다.

< 리스트 4. 정렬 후 binary_search 사용 >
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

using namespace std;

int main()
{
  vector<int> vec1;
  vec1.push_back(10);  vec1.push_back(20);  vec1.push_back(15);
  vec1.push_back(7);  vec1.push_back(100); vec1.push_back(40);
  vec1.push_back(11);  vec1.push_back(60);  vec1.push_back(140);

  sort( vec1.begin(), vec1.end() );

  bool bFind = binary_search( vec1.begin(), vec1.begin() + 5, 15 );
  if( false == bFind ) {
    cout << "15를 찾지 못했습니다." << endl;
  } else {
    cout << "15를 찾았습니다." << endl;
  }

  return 0;
}
< 결과 >

그림4

비교 조건자를 사용하는 경우는 위의 <리스트 2> 코드를 예를들면 <리스트 2>에서 사용했던 USER_MONEY_COMP 조건자를 사용합니다.

< 리스트 4. 리스트 2의 Users를 binary_search에 사용 >
…….
sort( Users.begin(), Users.end(), USER_MONY_COMP() );
bool bFind = binary_search( Users.begin(), Users.begin() + 3, User5, USER_MONY_COMP() );
…….
10.1.3 merge

두 개의 정렬된 구간을 합칠 때 사용하는 것으로 두 구간과 겹치지 않은 곳에 합친 결과를 넣어야 합니다. 주의해야 할 점은 합치기 전에 이미 정렬이 되어 있어야 하며 합친 결과를 넣는 것은 합치는 것들과 겹치면 안되며, 또한 합친 결과를 넣을 수 있는 공간을 확보하고 있어야 합니다.

merge의 원형
template<class InputIterator1, class InputIterator2, class OutputIterator>
   OutputIterator merge( InputIterator1 _First1, InputIterator1 _Last1,
      InputIterator2 _First2, InputIterator2 _Last2, OutputIterator _Result
   );

template<class InputIterator1, class InputIterator2, class OutputIterator, class BinaryPredicate>
   OutputIterator merge( InputIterator1 _First1, InputIterator1 _Last1,
    InputIterator2 _First2, InputIterator2 _Last2, OutputIterator _Result, BinaryPredicate _Comp );
merge 사용 방법
vector<int> vec1, vec2, vec3;
…..
merge( vec1.begin(), vec1.end(), vec2.begin(), vec2.end(), vec3.begin() );
아래의 <리스트 5>는 vec1과 vec2를 합쳐서 vec3에 넣는 것으로 vec1과 vec2는 이미 정렬되어 있고 vec3는 이 vec1과 vec2의 크기만큼의 공간을 미리 확보해 놓고 있습니다.

< 리스트 5. 두 개의 vector의 merge >
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

using namespace std;

int main()
{
  vector <int>::iterator Iter1;
  vector<int> vec1,vec2,vec3(12);
  
  for( int i = 0; i < 6; ++i )
    vec1.push_back( i );

  for( int i = 4; i < 10; ++i )
    vec2.push_back( i );
  
  cout << "vec1에 있는 값" << endl;
  for( Iter1 = vec1.begin(); Iter1 != vec1.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  cout << "vec2에 있는 값" << endl;
  for( Iter1 = vec2.begin(); Iter1 != vec2.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;


  merge( vec1.begin(), vec1.end(), 
      vec2.begin(), vec2.end(), 
      vec3.begin() );

  cout << "vec1과 vec2를 merge한 vec3에 있는 값" << endl;
  for( Iter1 = vec3.begin(); Iter1 != vec3.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  return 0;
}
< 결과 >

그림5

정렬 관련 알고리즘은 위에 소개한 세 개 이외에도 더 있지만 지면 관계상 보통 자주 사용하는 sort, binary_search, merge 세 개를 소개하는 것으로 마칩니다. 제가 소개하지 않은 정렬 관련 알고리즘을 더 공부하고 싶은 분들은 MSDN이나 C++ 책을 통해서 공부하시기를 바랍니다.

10.2. 범용 수치 알고리즘

10.2.1 accumulate

지정한 구간에 속한 값들을 모든 더한 값을 계산합니다. 기본적으로 더하기 연산만 하지만 조건자를 사용하면 더하기 이외의 연산도 할 수 있습니다. accumulate를 사용하기 위해서는 앞서 소개한 알고리즘과 다르게 <numeric> 헤더 파일을 포함해야 합니다.

accumulate의 원형
template<class InputIterator, class Type>
   Type accumulate( InputIterator _First, InputIterator _Last, Type _Val );
첫 번째와 두 번째 파라미터는 구간이며, 세 번째 파라미터는 구간에 있는 값에 더할 값입니다.
template<class InputIterator, class Type, class BinaryOperation>
 Type accumulate( InputIterator _First, InputIterator _Last, Type _Val, BinaryOperation _Binary_op );
네 번째 파라미터는 조건자로 조건자를 사용하여 기본 자료 형 이외의 데이터를 더할 수 있고, 더하기 연산이 아닌 다른 연산을 할 수도 있습니다.

accumulate 사용 방법
vector<int> vec1;
…..
// vec1에 있는 값들만 더 한다.
int Result = accmulate( vec1.begin(), vec1.end(), 0, );
아래의 <리스트 6>은 int를 저장하는 vector를 대상으로 accmurate를 사용하는 가장 일반적인 예입니다.

< 리스트 6. vector에 있는 값들을 계산 >
#include <vector>
#include <iostream>
#include <numeric>

using namespace std;

int main()
{
  vector <int>::iterator Iter1;
  vector<int> vec1;

  for( int i = 1; i < 5; ++i )
    vec1.push_back( i );

  // vec1에 있는 값
  for( Iter1 = vec1.begin(); Iter1 != vec1.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;


  // vec1에 있는 값들을 더한다.
  int Result1 = accumulate( vec1.begin(), vec1.end(), 0 );
  // vec1에 있는 값들을 더한 후 10을 더 한다.
  int Result2 = accumulate( vec1.begin(), vec1.end(), 10 );

  cout << Result1 << ", " << Result2 << endl;
}
< 결과 >

그림6

이번에는 조건자를 사용하여 유저 정의형을 저장한 vector를 accmulate에서 사용해 보겠습니다. 이번에도 더하기 연산만을 했지만 조건자를 사용하면 곱하기 연산 등도 할 수 있습니다.

< 리스트 7. 조건자를 사용한 accumulate >
#include <vector>
#include <iostream>
#include <numeric>

using namespace std;

struct USER
{
  int UID;
  int Level;
  int Money;
};

struct USER_MONY_ADD
{
  USER operator()(const USER& user1, const USER& user2)
  {
    USER User;
    User.Money = user1.Money + user2.Money;
    return User;
  }
};

int main()
{
  USER User1; User1.UID = 1;  User1.Money = 2000;
  USER User2; User2.UID = 2;  User2.Money = 2050;
  USER User3; User3.UID = 3;  User3.Money = 2200;
  USER User4; User4.UID = 4;  User4.Money = 1000;
  USER User5; User5.UID = 5;  User5.Money = 2030;

  vector<USER> Users;
  Users.push_back( User1 );  Users.push_back( User2 );
  Users.push_back( User3 );  Users.push_back( User4 );
  Users.push_back( User5 );

  vector <USER>::iterator Iter1;

  for( Iter1 = Users.begin(); Iter1 != Users.end(); ++Iter1 ) {
    cout << Iter1->UID << " : " << Iter1->Money << ", ";
  }
  cout << endl << endl;

  // Users에 있는 Money 값만 더하기 위해 Money가 0인 InitUser를 세 번째 파라미터에
  // 조건자를 네 번째 파라미터로 넘겼습니다.
  USER InitUser; InitUser.Money = 0;
  USER Result = accumulate( Users.begin(), Users.end(), InitUser, USER_MONY_ADD() );
  cout << Result.Money << endl;
}
< 결과 >

그림7

10.2.2 inner_product

두 입력 시퀀스의 내적을 계산하는 알고리즘으로 기본적으로는 +와 *을 사용합니다. 두 입력 시퀀스의 값은 위치의 값을 서로 곱한 값을 모두 더 한 것이 최종 계산 값이 됩니다. 주의 해야 할 것은 두 입력 시퀀스의 구간 중 두 번째 시퀀스는 첫 번째 시퀀스 구간 보다 크거나 같아야 합니다. 즉 첫 번째 시퀀스 구간의 데이터는 5개가 있는데 두 번째 시퀀스에 있는 데이터가 5개 보다 작으면 안됩니다.

inner_product의 원형
template<class InputIterator1, class InputIterator2, class Type>
   Type inner_product( InputIterator1 _First1, InputIterator1 _Last1, InputIterator2 _First2, 
      Type _Val );
조건자를 사용하는 버전으로 조건자를 사용하면 유저 정의형을 사용할 수 있는 내적 연산 방법을 바꿀 수 있습니다.
template<class InputIterator1, class InputIterator2, class Type,
   class BinaryOperation1, class BinaryOperation2>
   Type inner_product( InputIterator1 _First1, InputIterator1 _Last1, InputIterator2 _First2, 
      Type _Val, BinaryOperation1 _Binary_op1, BinaryOperation2 _Binary_op2 );
아래의 <리스트 8>은 조건자를 사용하지 않는 inner_product를 사용하는 것으로 vec1과 vec2의 내적을 계산합니다.

< 리스트 8. inner_product를 사용하여 내적 계산 >
#include <vector>
#include <iostream>
#include <numeric>

using namespace std;

int main()
{
  vector<int> vec1;
  for( int i = 1; i < 4; ++i )
    vec1.push_back(i);

  vector<int> vec2;
  for( int i = 1; i < 4; ++i )
    vec2.push_back(i);

  int Result = inner_product( vec1.begin(), vec1.end(), vec2.begin(), 0 );
  cout << Result << endl;

  return 0;
}
< 결과 >

그림8

<리스트 8>의 vec1과 vec2에는 각각 1, 2, 3 의 값이 들어가 있습니다. 이것을 네 번째 파라미터의 추가 값을 0을 넘긴 inner_droduct로 계산하면

14 = 0 + (1 * 1) + (2 * 2) + (3 * 3);

가 됩니다.

<리스트 8>의 코드를 보기 전에는 어떻게 계산 되는지 잘 이해가 되지 않는 분들은 <리스트 8> 코드를 보면 inner_product가 어떻게 계산 되는지 쉽게 이해할 수 있을 것입니다.

inner_product도 다른 알고리즘처럼 조건자를 사용할 수 있습니다. 제가 앞서 다른 알고리즘에서 조건자를 사용한 예를 보여 드렸으니 inner_product에서 조건자를 사용하는 방법은 숙제로 남겨 놓겠습니다.^^

범용 수치 알고리즘에는 위에 설명한 accmulate와 inner_product 이외에도 더 있지만 다른 것들은 보통 사용 빈도가 높지 않고 그것들을 다 소개하기에는 많은 지면이 필요로 하므로 이것으로 끝내겠습니다.^^;

범용 수치 알고리즘을 끝으로 STL의 알고리즘을 설명하는 것을 마치겠습니다. 이전 회와 이번에 걸쳐서 소개한 알고리즘은 STL에 있는 알고리즘 중 사용 빈도가 높은 알고리즘들로 이 것 이외에도 많은 알고리즘이 있으니 제가 설명한 알고리즘을 공부한 후에는 제가 설명하지 않은 알고리즘들도 공부하시기를 바랍니다.

지금까지 제가 설명한 글들을 보시면 STL은 사용할 때 일관성이 높다라는 것을 알 수 있을 것입니다. 높은 일관성 덕분에 하나를 알면 그 다음은 더 쉽게 알 수 있습니다.

C++의 STL은 결코 사용하기 어려운 것이 아닙니다. C++을 알고 있다면 아주 쉽게 공부할 수 있으며 STL을 사용함으로 더 쉽고 견고하게 프로그래밍할 수 있습니다. 그러나 STL의 컨테이너나 알고리즘에 대해서 잘 모르면서 다른 사람들이 사용한 코드를 보고 그냥 사용하면 STL이 독이 될 수도 있음을 조심해야 합니다.

저는 몇 달 전부터 C++0x을 공부하고 있습니다. C++0x는 현재 개발중인 새로운 C++ 표준입니다. C++0x에는 지금의 C++을 더 강력하고 쉽게 사용할 수 있게 해 주는 다양한 것들이 있습니다. 이 중 lambda라는 것이 있는데 이것을 사용하면 알고리즘에서 조건자를 사용할 때 지금보다 훨씬 더 편하게 기술할 수 있습니다. STL을 공부한 이 후에는 C++0x을 공부하시기를 바랍니다.


[출처 : http://www.hanb.co.kr/network/view.html?bi_id=1627]
제공 : 한빛 네트워크
저자 : 최흥배
이전기사 : 제가 한빛 네트워크에 C++ STL 글을 연재한지 벌써 1년이 넘었습니다. template부터 시작하여 이전 회까지는 STL의 컨테이너들에 대해 설명했습니다. 이제 알고리즘에 대한 것만 남았습니다. 제가 쓴 글들을 보고 C++ STL을 공부하는데 얼마나 도움이 되는지 알 수 없지만 조금이나마 도움이 되었기를 바랍니다.
앞으로 2회에 걸쳐서 STL의 알고리즘 중 많이 사용하고 있는 것을 중심으로 설명하겠습니다.
STL의 컨테이너는 그 자체만으로도 대단히 유용한 것이지만 알고리즘과 결합되면 유용성은 더욱 더 커집니다.
STL의 알고리즘은 컨테이너처럼 제네릭(총칭적)합니다. 제네릭 하다고 하는 이유는 STL에 있는 다양한 알고리즘을 한 종류의 컨테이너에만 사용할 수 있는 것이 아닌 vector, list, deque, map 등 여러 가지 다양한 컨테이너에 사용할 수 있기 때문입니다(참고로 STL 컨테이너만이 아닌 일반 배열 구조도 알고리즘에 사용할 수 있습니다).

9.1 STL 알고리즘 분류

STL 알고리즘은 크게 네 가지로 분류 할 수 있습니다.
  • 변경 불가 시퀀스 알고리즘
  • 변경 가능 시퀀스 알고리즘
  • 정렬 관련 알고리즘
  • 범용 수치 알고리즘
변경 불가 시퀀스 알고리즘에는 find와 for_each 등이 있으며 대상 컨테이너의 내용을 변경하지 못합니다. 변경 가능 시퀀스 알고리즘으로는 copy, generate 등이 있으며 대상 컨테이너 내용을 변경합니다.
정렬 관련 알고리즘은 정렬이나 머지를 하는 알고리즘으로 sort와 merge 등이 있습니다.
범용 수치 알고리즘은 값을 계산하는 알고리즘으로 accumulate 등이 있습니다.

STL의 모든 알고리즘을 설명하기에는 많은 지면이 필요하므로 위에 소개한 알고리즘 카테고리 별로 자주 사용하는 알고리즘 중심으로 어떤 기능이 있으며 어떻게 사용하는지 설명하겠습니다.

9.2 조건자

알고리즘 중에는 함수를 파라미터로 받아들이는 것이 많습니다. 알고리즘에서 함수를 파라미터로 받아들이지 않거나 기본 함수 인자를 사용하며 알고리즘을 사용하는 컨테이너의 데이터는 기본 자료 형만을 사용할 수 있습니다. 유저 정의형 자료 형(struct, class)을 담는 컨테이너를 알고리즘에서 사용하기 위해서는 관련 함수를 만들어서 파라미터로 넘겨줘야 합니다.
알고리즘에 파라미터로 넘기는 함수는 보통 함수보다는 함수객체를 사용하는 편입니다.

9.3 변경 불가 시퀀스 알고리즘

9.3.1. find

컨테이너에 있는 데이터 중 원하는 것을 찾기 위해서는 find 알고리즘을 사용하면 됩니다.
참고로 알고리즘을 사용하려면 algorithm 헤더 파일을 추가해야 합니다.
#include < algorithm >
find의 원형
template<class InputIterator, class Type>
InputIterator find( InputIterator _First, InputIterator _Last, const Type& _Val );
첫 번째 파라미터에 찾기를 시작할 반복자 위치, 두 번째 파라미터에 마지막 위치(반복자의 end()와 같이 이 위치의 바로 앞까지 검색함), 세 번째 파라미터에는 찾을 값을 넘깁니다.
find가 성공하면 데이터가 있는 위치를 가리키는 반복자를 반환하고, 실패하면 반복자의 end()를 반환합니다.

find 사용 방법
vector< int > ItemCodes;
……..
// ItemCodes 컨테이너의 시작과 끝 사이에서 15를 찾는다.
find( ItemCodes.begin(), ItemCodes.end(), 15 );
find를 사용하여 캐릭터의 아이템을 검색하는 예제 코드 입니다.

< 리스트 1. 캐릭터 아이템 검색 >
#include <algorithm>
#include <vector>
#include <iostream>

using namespace std;


int main()
{
  vector< int > CharItems;
  CharItems.push_back( 12 );
  CharItems.push_back( 100 );
  CharItems.push_back( 77 );

  vector< int >::iterator FindIter;

  // CharItems의 처음과 끝에서 12를 찾는다.
  FindIter = find( CharItems.begin(), CharItems.end(), 12 );
  if( FindIter != CharItems.end() )
  {
    cout << "CharItem 12를 찾았습니다." << endl;
  }
  else
  {
    cout << "CharItem 12는 없습니다" << endl;
  }

  // CharItems 두 번째와 끝에서12를 찾는다
  // ++CharItems.begin()로 반복자를 한칸 이동 시켰습니다.
        FindIter = find( ++CharItems.begin(), CharItems.end(), 12 );
  if( FindIter != CharItems.end() )
  {
    cout << "CharItem 12를 찾았습니다." << endl;
  }
  else
  {
    cout << "CharItem 12는 없습니다" << endl;
  }

  return 0;
}
< 결과 >

1

<리스트 1>에서는 vector 컨테이너를 대상으로 했지만 vector 이외의 list, deque도 같은 방식으로 사용합니다.
다만 map, set, hash_map의 연관 컨테이너는 해당 컨테이너의 멤버로 find()를 가지고 있으므로 알고리즘에 있는 find가 아닌 해당 멤버 find를 사용합니다.

9.3.2 find_if

find를 사용하면 컨테이너에 있는 데이터 중 찾기 원하는 것을 쉽게 찾을 수 있습니다. 그런데 find만으로는 많이 부족합니다. 이유는 find는 기본형만을 컨테이너에 저장하고 있을 때만 사용할 수 있습니다. 만약 유저 정의형을 저장하는 컨테이너는 find를 사용할 수 없습니다. 이럴 때는 9.2에서 짧게 설명한 조건자를 사용해야 합니다. 즉 조건자에 어떤 방식으로 찾을지 정의하고 알고리즘에서 이 조건자를 사용합니다.
find_if는 기본적으로 find와 같습니다. 다른 점은 조건자를 받아들인다는 것입니다.

find_if 원형
template<class InputIterator, class Predicate>
InputIterator find_if( InputIterator _First, InputIterator _Last, Predicate _Pred );
find와 비교하면 첫 번째와 두 번째 파라미터는 동일합니다. 세 번째 파라미터가 다릅니다. find는 세 번째 파라미터에 찾기 원하는 값을 넘기지만 find_if는 조건자를 넘깁니다.

find_if 사용법
// 컨테이너에 저장할 유저 정의형 
struct User
{
    …….
    int Money;
    int MobKillCount;
    ……
};

// 조건자
struct FindBestUser()
{
   …………………….
};

vector< User > Users;
……..

find_if( Users.begin(), Users.end(), FindBestUser() );
조건자가 있다는 것 이외에는 find_if는 find와 같으므로 조건자 부분만 잘 보시면 됩니다.

< 리스트 2. 특정 돈을 가지고 있는 유저 찾기 >
#include <algorithm>
#include <vector>
#include <iostream>

using namespace std;

struct User
{
  int Money;
  int Level;
};

struct FindMoneyUser
{
  bool operator() ( User& user ) const { return user.Money == ComparMoney; }
  int ComparMoney;
};

int main()
{
  vector< User > Users;

  User user1; user1.Level = 10; user1.Money = 2000;
  User user2; user2.Level = 5;  user2.Money = -10;
  User user3; user3.Level = 20; user3.Money = 35000;

  Users.push_back( user1 );
  Users.push_back( user2 );
  Users.push_back( user3 );

  vector< User >::iterator FindUser;

  FindMoneyUser tFindMoneyUser; 
  tFindMoneyUser.ComparMoney = 2000;
  FindUser = find_if( Users.begin(), Users.end(), tFindMoneyUser );
  if( FindUser != Users.end() )
  {
    cout << "소지하고 있는 돈은: " << FindUser->Money << "입니다" << endl;
  }
  else
  {
    cout << " 유저가 없습니다. " << endl;
  }

  return 0;
}
< 결과 >

2

<리스트 2>에서는 조건자 함수를 함수 포인터가 아닌 함수 객체를 사용하였습니다. 함수 객체를 사용한 이유는 함수 객체는 inline화 되므로 함수 포인터와 비교하면 함수 포인터 참조와 함수 호출에 따른 작업이 필요 없으며, 함수 객체를 사용하면 객체에 특정 데이터를 저장할 수 있어서 조건자가 유연해집니다.

<리스트 2>의 FindMoneyUser를 일반 함수로 만들었다면 비교로 사용할 ComparMoney의 값은 함수를 정의할 때 고정됩니다. 그러나 함수 객체로 만들면 객체를 생성할 때 ComparMoney의 값을 설정할 수 있어서 유연해집니다.

<참고>
조건자를 사용하는 알고리즘 : STL의 알고리즘은 조건자를 사용하지 않는 것과 조건자를 사용하는 알고리즘 두 가지 버전을 가지고 있는 알고리즘이 있습니다. 이중 조건자를 사용하는 알고리즘은 보통 조건자를 사용하지 않는 알고리즘의 이름에서 뒤에 '_if'를 붙인 이름으로 되어 있습니다.

9.3.3. for_each

for_each는 순차적으로 컨테이너들에 담긴 데이터를 함수의 파라미터로 넘겨서 함수를 실행시키는 알고리즘입니다.
온라인 게임에서 예를 들면 플레이하고 있는 유저 객체를 vector 컨테이너인 Users에 저장하고 모든 유저들의 현재 플레이 시간을 갱신하는 기능을 구현한다면 플레이 시간을 계산하는 함수 객체 UpdatePlayTime을 만든 후 for_each에 Users의 시작과 끝 반복자와 UpdatePlayTime 함수 객체를 넘깁니다.

for_each 원형
template<class InputIterator, class Function>
Function for_each( InputIterator _First, InputIterator _Last, Function _Func );
첫 번째 파라미터는 함수의 파라미터로 넘길 시작 위치, 두 번째 파라미터는 함수의 인자로 넘길마지막 위치, 세 번째는 컨테이너의 데이터를 파라미터로 받을 함수입니다.

for_each 사용 방법
// 함수 객체
struct UpdatePlayTime
{
……….
};

vector< User > Users;

for_each( Users.begin(), Users.end(), UpdatePlayTime );
아래는 위에서 예를 들었던 것을 완전한 코드로 구현한 예제 코드입니다.

< 리스트 3. for_each를 사용하여 유저들의 플레이 시간 갱신 >
#include <algorithm>
#include <vector>
#include <iostream>

using namespace std;

struct User
{
  int UID;
  int PlayTime;
};

struct UpdatePlayTime
{
  void operator() ( User& user )
  {
    user.PlayTime += PlayTime;
  }

  int PlayTime;
};

int main()
{
  vector< User > Users;

  User user1; user1.UID = 1; user1.PlayTime = 40000;
  User user2; user2.UID = 2; user2.PlayTime = 0;
  User user3; user3.UID = 3; user3.PlayTime = 25000;

  Users.push_back( user1 );
  Users.push_back( user2 );
  Users.push_back( user3 );

  // 현재 플레이 시간
  vector< User >::iterator IterUser;
  for( IterUser = Users.begin(); IterUser != Users.end(); ++IterUser )
  {
    cout << "UID : " << cout << IterUser->UID << "의 총 플레이 시간: " << IterUser->PlayTime << endl;
  }
  cout << endl;

  UpdatePlayTime updatePlayTime;
  updatePlayTime.PlayTime = 200;

  // 두 번째 유저부터 갱신
  for_each( Users.begin() + 1, Users.end(), updatePlayTime );
  
  for( IterUser = Users.begin(); IterUser != Users.end(); ++IterUser )
  {
    cout << "UID : " << cout << IterUser->UID << "의 총 플레이 시간: " << IterUser->PlayTime << endl;
  }

  return 0;
}
< 결과 >

3

변경 불가 시킨스 알고리즘에는 위에 설명한 find, find_if, for_each 이외에 count, search, equal, adjacent_find, equal 등이 있습니다.

9.4. 변경 가능 시퀀스 알고리즘

9.3의 알고리즘들이 대상이 되는 컨테이너에 저장한 데이터를 변경하지 않는 것들이라면 이번에 설명한 알고리즘들은 제목대로 대상이 되는 컨테이너의 내용을 변경하는 알고리즘들입니다. 컨테이너에 복사, 삭제, 대체 등을 할 때는 이번에 소개할 알고리즘들을 사용합니다.

9.4.1. generate

컨테이너의 특정 구간을 특정 값으로 채우고 싶을 때가 있습니다. 이 값이 동일한 것이라면 컨테이너의 assign() 멤버를 사용하면 되지만 동일한 값이 아니라면 assign()을 사용할 수 있습니다.
이 때 사용하는 알고리즘이 generate입니다. generate 알고리즘에 값을 채울 컨테이너의 시작과 끝, 값을 생성할 함수를 파라미터로 넘깁니다.

generate 원형
template<class ForwardIterator, class Generator>
void generate( ForwardIterator _First, ForwardIterator _Last, Generator _Gen );
첫 번째 파라미터는 값을 채울 컨테이너의 시작 위치의 반복자, 두 번째는 컨테이너의 마지막 위치의 반복자, 세 번째는 값을 생성할 함수 입니다.

generate 사용 방법
// 값 생성 함수
struct SetUserInfo
{
  …….
};

vector< User > Users(5);

generate( Users.begin(), Users.end(), SetUserInfo() );
generate 알고리즘의 대상이 되는 컨테이너는 값을 채울 공간이 미리 만들어져 있어야 합니다. 즉 generate는 컨테이너에 데이터를 추가하는 것이 아니고 기존의 데이터를 다른 값으로 변경하는 것입니다.

< 리스트 4. generate를 사용하여 유저의 기초 데이터 설정>
struct User
{
  int UID;
  int RaceType;
  int Sex;
  int Money;
};

struct SetUserInfo
{
  SetUserInfo() { UserCount = 0; }

  User operator() ()
  {
    User user;

    ++UserCount;
    
    user.UID = UserCount;
    user.Money = 2000;

    if( 0 == (UserCount%2) )
    {
      user.RaceType = 1;
      user.Sex = 1;
      user.Money += 1000;
    }
    else
    {
      user.RaceType = 0;
      user.Sex = 0;
    }

    return user;
  }

  int UserCount;
};

int main()
{
  vector< User > Users(5);

  generate( Users.begin(), Users.end(), SetUserInfo() );
  
  char szUserInfo[256] = {0,};

  vector< User >::iterator IterUser;
  for( IterUser = Users.begin(); IterUser != Users.end(); ++IterUser )
  {
    sprintf( szUserInfo, "UID %d, RaceType : %d, Sex : %d, Money : %d",
    IterUser->UID, IterUser->RaceType, IterUser->Sex, IterUser->Money );

    cout << szUserInfo << endl;
  }
  
  return 0;
}
< 결과 >

4

9.4.2. copy

copy 알고리즘은 컨테이너에 저장한 것과 같은 자료 형을 저장하는 다른 컨테이너에 복사하고 싶을 때 사용합니다.

컨테이너 A의 데이터를 컨테이너 B에 copy 하는 경우 컨테이너 B에 데이터를 추가하는 것이 아니고 덧쓰는 것이므로 A에서 10개를 복사하는 경우 B에는 10개만큼의 공간이 없다면 버그가 발생합니다. 또 A와 B 컨테이너는 같은 컨테이너 필요는 없습니다만 당연히 컨테이너에 저장하는 자료 형은 같아야 합니다.

copy 원형
template<class InputIterator, class OutputIterator>
   OutputIterator copy(
      InputIterator _First, 
      InputIterator _Last, 
      OutputIterator _DestBeg
   );
copy 사용 방법
vector<int> vec1;
……..
vector<int> vec2;

copy( vec1.begin(), vec1.end(), vec2.begin() );
< 리스트 5. copy 알골리즘 사용 예>
#include <algorithm>
#include <vector>
#include <list>
#include <iostream>

using namespace std;

int main()
{
  vector<int> vec1(10);
  generate( vec1.begin(), vec1.end(), rand );

  cout << "vec1의 모든 데이터를 vec2에 copy" << endl;

  vector<int> vec2(10);
  copy( vec1.begin(), vec1.end(), vec2.begin() );
  for( vector<int>::iterator IterPos = vec2.begin();
    IterPos != vec2.end();
    ++IterPos )
  {
    cout << *IterPos << endl;
  }
  cout << endl;


  cout << "vec1의 모든 데이터를 list1에 copy" << endl;
  list<int> list1(10);
  copy( vec1.begin(), vec1.end(), list1.begin() );
  
  for( list<int>::iterator IterPos2 = list1.begin();
    IterPos2 != list1.end();
    ++IterPos2 )
  {
    cout << *IterPos2 << endl;
  }

  return 0;
}
< 결과 >

5

9.4.4. remove

remove 알고리즘은 컨테이너에 있는 특정 값들을 삭제하고 싶은 때 사용합니다.
주의해야 될 점은 삭제 후 크기가 변하지 않는다는 것입니다. 삭제가 성공하면 삭제 대상이 아닌 데이터들을 앞으로 몰아 넣고 마지막 위치의(컨테이너의 end()가 아닌 삭제 후 빈 공간에 다른 데이터를 쓰기 시작한 위치) 반복자를 반환합니다. 리턴 값이 가리키는 부분부터 끝(end()) 사이의 데이터 순서는 정의되어 있지 않으면 진짜 삭제를 하기 위해서는 erase()를 사용해야 합니다.

remove 원형
template<class ForwardIterator, class Type>
ForwardIterator remove( ForwardIterator _First, ForwardIterator _Last, const Type& _Val );
첫 번째 파라미터는 삭제 대상을 찾기 시작할 위치의 반복자, 두 번째 파라미터는 삭제 대상을 찾을 마지막 위치의 반복자, 세 번째 파라미터는 삭제를 할 값입니다.

remove 사용 방법
vector<int> vec1;
…..
remove( vec1.begin(), vec1.end(), 20 );
< 리스트 6. remove 사용 예>
#include <algorithm>
#include <vector>
#include <list>
#include <iostream>

using namespace std;

int main()
{
  vector<int> vec1;
  vec1.push_back(10);  vec1.push_back(20);  vec1.push_back(20);
  vec1.push_back(40); vec1.push_back(50);  vec1.push_back(30);  

  vector<int>::iterator iterPos;

  cout << "vec1에 있는 모든 데이터 출력" << endl;
  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }
  cout << endl;

  cout << "vec1에서 20 remove" << endl;
  vector<int>::iterator RemovePos = remove( vec1.begin(), vec1.end(), 20 );

  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }
  cout << endl;

  cout << "vec1에서 20 remove 이후 사용 하지않는 영역 완전 제거" << endl;
  while( RemovePos != vec1.end() )
  {
    RemovePos = vec1.erase( RemovePos );
  }

  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }
}
< 결과 >

6

<리스트 6>에서는 remove 후 erase()로 완전하게 제거하는 예를 나타내고 있습니다.
vector<int>::iterator RemovePos = remove( vec1.begin(), vec1.end(), 20 );
로 삭제 후 남은 영역의 반복자 위치를 받은 후
while( RemovePos != vec1.end() )
{
  RemovePos = vec1.erase( RemovePos );
}
로 완전하게 제거하고 있습니다.

9.4.5. replace

컨테이너의 특정 값을 다른 값으로 바꾸고 싶을 때는 replace 알고리즘을 사용합니다.

replace 원형
template<class ForwardIterator, class Type>
   void replace(
      ForwardIterator _First, 
      ForwardIterator _Last,
      const Type& _OldVal, 
      const Type& _NewVal
   );
replace 사용 방법
vector<int> vec1;
……
replace( vec1.begin(), vec1.end(), 20, 30 );
< 리스트 7. replace 사용 예>
#include <algorithm>
#include <vector>
#include <list>
#include <iostream>

using namespace std;

int main()
{
  vector<int> vec1;
  vec1.push_back(10);  vec1.push_back(20);  vec1.push_back(20);
  vec1.push_back(40); vec1.push_back(50);  vec1.push_back(30);

  vector<int>::iterator iterPos;

  cout << "vec1에 있는 모든 데이터 출력" << endl;
  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }
  cout << endl;

  cout << "vec1의 세 번째 요소부터 20을 200으로 변경" << endl;
  replace( vec1.begin() + 2, vec1.end(), 20, 200 );

  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }

  return 0;
}
< 결과 >

7

변경 가능 시퀀스 알고리즘에는 generate, copy, remove, replace 이외에도 fill, swap, reverse, transform 등이 있으니 제가 설명하지 않은 알고리즘들은 다음에 여유가 있을 때 공부하시기를 바랍니다.

지금까지 설명한 알고리즘을 보면 어떤 규칙이 있다는 것을 알 수 있을 것입니다. 알고리즘에 넘기는 파라미터는 알고리즘을 적용할 컨테이너의 위치를 가리키는 반복자와 특정 값 or 조건자를 파라미터로 넘기고 있습니다. 그래서 각 알고리즘은 이름과 기능이 다를 뿐 사용 방법은 대체로 비슷합니다. 즉 사용 방법에 일괄성이 있습니다. 그래서 하나의 알고리즘만 사용할 줄 알게 되면 나머지 알고리즘은 쉽게 사용하는 방법을 배웁니다. 이런 것이 바로 STL의 장점이겠죠

이번 회는 이것으로 설명을 마치고 남은 알고리즘은 다음 회에 이어서 계속 설명하겠습니다.


[출처 : http://www.hanb.co.kr/network/view.html?bi_id=1626]
제공 : 한빛 네트워크
저자 : 최흥배
이전기사 : 앞서 설명했던 map과 비슷하면서도 다른 set을 이번에 이야기 하려고 합니다. map 이전에는 hash_map을 설명했는데 이번에 이야기할 set과 여러 부분에서 중복되는 부분이 있고, 저는 현업에서 set을 사용하는 경우가 거의 없어서 이번에는 내용이 길지 않을 것 같습니다.

8.1 set 이란

set은 원하는 key를 신속하게 찾고, 또 이 key가 정렬되기를 원할 때 사용합니다.(여기서 말하는 key라는 것은 저장할 자료를 말합니다). map과 비슷하지만 다른 점은 map은 key와 값이 한 쌍으로 저장하지만 set은 key만 저장합니다. set도 map과 같이 key를 중복으로 저장할 수 없습니다. 만약 key를 중복으로 사용하고 싶다면 multiset을 사용해야 합니다. 사용방법은 set과 거의 같습니다. set은 map과 같이 이진 탐색 트리 자료구조를 사용합니다.

8.2 set을 사용할 때

앞서 이야기 했듯이 set은 자료를 저장할 때 내부에서 자동으로 정렬하고, map과 다르게 key만 저장합니다.

set은 아래 조건일 때 사용하면 좋습니다.
  1. 정렬해야 한다.
  2. key가 있는지 없는지 알아야 할 때
  3. 많은 자료를 저장하고, 검색 속도가 빨라야 할 때
다른 컨테이너를 설명 할 때 꽤 많은 이야기를 했는데 그동안 했던 이야기와 중복되는 것이 많고 특히 앞의 map과 비슷한 부분이 많아서 이번에는 바로 사용 방법으로 들어가겠습니다.

8.3 set 사용 방법

set 컨테이너를 쓰려면 먼저 헤더 파일을 포함해야 합니다.
#include <set>
보통 set을 사용하는 방법은 아래와 같습니다.
set< key 자료 type > 변수 이름

set< int > set1;
map과 사용 방법이 비슷하죠? 다만, set은 위와 같이 key만 저장합니다. 위에서는 key로 int 타입을 사용했습니다.

set은 map과 같이 기본적으로 오름차순으로 정렬을 합니다. 만약 이것을 내림 차순으로 바꾸고 싶거나 key 자료형이 기본형이 아니란 유저 정의형이라면 함수 객체로 정렬 방법을 제공해야 합니다.

set의 key가 기본형이고 내림 차순으로 정렬하고 싶다면 STL의 greater 알고리즘을 사용하면 됩니다.
set< key 자료 type, 비교 함수 > 변수 이름

set< int, greater<int> > set1;
만약 key가 기본형이 아니고 Player 이라는 클래스를 사용하고 Player의 멤버 중 HP를 비교하여 정렬하고 싶다면 아래와 같이 하면 됩니다.
class Player
{
public:
   Player() {}
   ~Player() {}

   int m_HP;
};

template< typename T >
struct HP_COMPARE : public binary_function< T, T, bool >
{
   bool operator() (T& player1, T& player2) const 
   { 
      return player1.m_HP > player2.m_HP;
   }
};

int main()
{
   set< Player, HP_COMPARE<Player> > set1;
   return 0;
}
8.3.1 set의 주요 멤버들

멤버 설명
begin 첫 번째 원소의 랜덤 접근 반복자를 반환
clear 저장하고 있는 모든 원소를 삭제
empty 저장 하고 있는 요소가 없으면 true 반환
end 마지막 원소 다음의(미 사용 영역) 반복자를 반환
erase 특정 위치의 원소나 지정 범위의 원소들을 삭제
find key와 연관된 원소의 반복자 반환
insert 원소 추가
lower_bound 지정한 key의 요소를 가지고 있다면 해당 위치의 반복자를 반환
operator[] 지정한 key 값으로 원소 추가 및 접근
rbegin 역방향으로 첫 번째 원소의 반복자를 반환
rend 역방향으로 마지막 원소 다음의 반복자를 반환
size 원소의 개수를 반환
upper_bound 지정한 key 요소를 가지고 있다면 해당 위치 다음 위치의 반복자 반환
[표 1]. map의 주요 멤버들

8.3.2. 추가

set 에서는 자료를 추가 할 때 insert를 사용합니다.
원형 : 
  pair <iterator, bool> insert( const value_type& _Val );
  iterator insert( iterator _Where, const value_type& _Val );
  template<class InputIterator> void insert( InputIterator _First, InputIterator _Last );
첫 번째가 자주 사용하는 방식입니다.
set< int > set1;

// key 1을 추가.
set1.insert( 1 );

// 추가했는지 조사 하고 싶을 때는
pair< set<int>::iterator, bool > Result;
Result = set1.insert( 1 );
if( Result.second )
{
   // 추가 성공
}
else
{
  // 추가 실패
}
두 번째 방식은 특정 위치에 추가할 수 있습니다.
// 첫 번째 위치에 key 1, value 35를 추가
set1.insert( set1.begin(), 10 );
세 번째 방식은 지정한 반복자 구간에 있는 것들을 추가합니다.
set< int > set2;
// set1의 모든 요소를 set2에 추가.
set2.insert( set1.begin(), set1.end() );
set은 이미 있는 key 값을 추가할 수 없습니다(복수의 key 값을 사용하기 위해서는 multiset을 사용해야 합니다).

참고로 특정 위치를 지정하여 추가를 하여도 정렬되어 저장합니다.

<리스트 1. 특정 위치에 추가했을 때의 정렬 여부>
#include <iostream>
#include <functional>
#include <set>

using namespace std;

int main()
{
   set< int > set1;
   set1.insert( 10 );
   set1.insert( 15 );
   set1.insert( 12 );
   set1.insert( 2 );
   set1.insert( 100 );

   for( set<int>::iterator IterPos = set1.begin();
      IterPos != set1.end(); ++IterPos )
   {
      cout << *IterPos << endl;
   }

   set<int>::iterator IterPos = set1.begin();
   ++IterPos;
   set1.insert( IterPos, 90 );
   
   cout << endl;
   cout << "90을추가후set1의모든요소출력" << endl;
   for( set<int>::iterator IterPos = set1.begin();
      IterPos != set1.end(); ++IterPos )
   {
      cout << *IterPos << endl;
   }
   
   return 0;
}
<결과>

그림1

8.3.3. 반복자 사용

다른 컨테이너와 같이 정 방향 반복자 begin(), end()와 역 방향 반복자 rbegin(), rend()를 지원합니다. 사용 방법은 다음과 같습니다.
// 정 방향으로 set1의 모든 Key 출력
set< int >::iterator Iter_Pos;
for( Iter_Pos = set1.begin(); Iter_Pos != set1.end(); ++Iter_Pos)
{
   cout << *Iter_Pos << endl;
}

// 역 방향으로 set1의 모든 요소 Key 출력
set< int >::reverse_iterator Iter_rPos;
for( Iter_rPos = set1.rbegin(); Iter_rPos != set1.rend(); ++Iter_rPos)
{
   cout << *Iter_rPos << endl;
}
8.3.4. 검색

set에서 검색은 key를 대상으로 합니다.
key와 같은 요소를 찾으면 그 요소의 반복자를 반환하고, 찾지 못한 경우에는 end()를 가리키는 반복자를 반환합니다.
원형 : 
  iterator find( const Key& _Key );
  const_iterator find( const Key& _Key ) const;
두 방식의 차이는 반환된 반복자가 const 여부입니다. 첫 번째 방식은 const가 아니므로 찾은 요소의 Key를 변경할 수 있습니다. 그러나 두 번째 방식은 Key를 변경할 수 없습니다.
// key가 10인 요소 찾기.
set< int >::Iterator FindIter = set1.find( 10 );

// 찾았다면 value를 1000으로 변경
if( FindIter != set1.end() )
{
   // Key를 찾았다!!!
}
set은 map과 다르게 Key만 저장하기에 Key의 변경이 가능하지만 find로 찾은 Key를 변경하면 정렬되지 않습니다.

<리스트 2. find로 찾은 Key 변경>
int main()
{
   set< int > set1;
   set1.insert( 10 );
   set1.insert( 15 );
   set1.insert( 12 );

   for( set<int>::iterator IterPos = set1.begin();
      IterPos != set1.end(); ++IterPos )
   {
      cout << *IterPos << endl;
   }

   set<int>::iterator FindIter = set1.find( 15 );
   if( FindIter != set1.end() )
   {
      *FindIter = 11;
   }

   cout << endl;
   cout << "15를 검색 후11로 변경한 후 set1의 모든 요소 출력" << endl;
   for( set<int>::iterator IterPos = set1.begin();
      IterPos != set1.end(); ++IterPos )
   {
      cout << *IterPos << endl;
   }

   return 0;
}
<결과>

그림2

8.3.5. 삭제

저장하고 있는 요소를 삭제할 때는 erase와 clear를 사용합니다.
erase는 특정 요소를 삭제할 때 사용하고, clear는 모든 요소를 삭제할 때 사용합니다.

erase
원형 : 
  iterator erase( iterator _Where );
  iterator erase( iterator _First, iterator _Last );
  size_type erase( const key_type& _Key );
첫 번째 방식은 특정 위치에 있는 요소를 삭제합니다.
// 두 번째 위치의 요소 삭제.
set1.erase( ++set1.begin() );
두 번째 방식은 지정한 구역 에 있는 요소들을 삭제합니다.
// set1의 처음과 마지막에 있는 모든 요소 삭제
set1.erase( set1.begin(), set1.end() );
세 번째 방식은 지정한 Key와 같은 요소를 삭제합니다.
// key가 10인 요소 삭제.
set1.erase( 10 );
첫 번째와 두 번째 방식으로 삭제를 하면 삭제되는 요소의 다음을 가리키는 반복자를 반환합니다.
세 번째 방식은 삭제된 개수를 반환하는데 정말 삭제가 되었다면 1이 반환됩니다. 그러나 multiset에서는 1이 아닌 삭제된 개수를 반환합니다.

clear

set의 모든 요소를 삭제할 때는 clear를 사용합니다.
set1.clear();
이것으로 set에서 자주 사용하는 멤버들에 대한 설명은 끝났습니다.
아래의 <리스트 3>은 set을 전반적으로 사용하는 예를 나타내고 있습니다.

<리스트 3. set 사용 예>
#include <iostream>
#include <functional>
#include <set>

using namespace std;

class Player
{
public:
   Player() {}
   ~Player() {}

   int m_Level;
};

// 레벨이 높은 순으로 정렬
template< typename T > 
struct LEVEL_COMPARE : public binary_function< T, T, bool >
{
   bool operator() (const T& player1, const T& player2) const 
   { 
      return player1->m_Level > player2->m_Level;
   }
};

int main()
{
   set< Player*, LEVEL_COMPARE<Player*> > PlayerList;

   Player* pPlayer1 = new Player; pPlayer1->m_Level = 10;
   PlayerList.insert( pPlayer1 );
   Player* pPlayer2 = new Player; pPlayer2->m_Level = 45;
   PlayerList.insert( pPlayer2 );
   Player* pPlayer3 = new Player; pPlayer3->m_Level = 5;
   PlayerList.insert( pPlayer3 );
   Player* pPlayer4 = new Player; pPlayer4->m_Level = 15;
   PlayerList.insert( pPlayer4 );

   // 정 방향으로 출력( 레벨이 높은 순으로)
   for( set< Player*, LEVEL_COMPARE<Player*> >::iterator IterPos = PlayerList.begin();
      IterPos != PlayerList.end(); ++IterPos )
   {
      cout << (*IterPos)->m_Level << endl;
   }

   cout << endl;

   // 역 방향으로 출력( 레벨이 낮은 순으로)
   for( set< Player*, LEVEL_COMPARE<Player*> >::reverse_iterator IterPos = PlayerList.rbegin();
      IterPos != PlayerList.rend(); ++IterPos )
   {
      cout << (*IterPos)->m_Level << endl;
   }

   cout << endl;

   // pPlayer4를검색
   set< Player*, LEVEL_COMPARE<Player*> >::iterator FindPlayer = PlayerList.find( pPlayer4 );
   if( FindPlayer != PlayerList.end() )
   {
      cout << "pPlayer4를 찾았습니다" << endl;

      cout << "pPlayer4 삭제" << endl;
      PlayerList.erase( FindPlayer );
   }
   else
   {
      cout << "pPlayer4를 못찾았습니다" << endl;
   }

   cout << endl;
   cout << "Total Player Count : " << PlayerList.size() << endl;

   cout << endl;
   PlayerList.clear();
   if( PlayerList.empty() )
   {
      cout << "Player가 없습니다." << endl;
   }
   
   delete pPlayer1;
   delete pPlayer2;
   delete pPlayer3;
   delete pPlayer4;
   return 0;
}
<결과>

그림3

이전 회의 map과 거의 대부분 비슷하기 때문에 map을 아시는 분들은 아주 쉬웠을 것이라고 생각합니다.

이전과 같이 set의 멤버 중 설명하지 않은 것들은 MSDN에 있는 set 설명을 참조하시기를 바랍니다.

과제

Sset은 Key를 중복으로 저장할 수 없습니다. 중복 Key를 저장하기 위해서는 multiset을 사용해야 합니다. multiset을 공부해 보세요

참고 url : http://msdn.microsoft.com/ko-kr/library/w5txk7zc.aspx


[출처 : http://www.hanb.co.kr/network/view.html?bi_id=1624]
제공 : 한빛 네트워크
저자 : 최흥배
이전기사 : 이번에는 이전 회에 설명한 hash_map과 같은 연관 컨테이너 중의 하나인 map에 대해서 설명합니다. 사용법이 hash_map과 대부분 비슷해서 앞으로 할 이야기가 별로 어렵지 않을 것입니다.

이전 회에서 설명했던 것은 가급적 또 설명하지 않을 테니 앞의 글들을 보지 않은 분들은 꼭 봐 주세요. 그럼 map에 대한 이야기를 시작 하겠습니다.

7.1 map의 자료구조

map의 자료구조는 '트리(tree)'입니다(정확하게 말하면 트리 자료구조 중의 하나인 '레드-블랙 트리(Red-Black tree)'입니다).

트리는 한글로 '나무'입니다. 나무는 뿌리에서 시작하여 여러 갈래의 가지가 있고, 가지의 끝에는 나무 잎이 있습니다. 트리 자료구조도 이와 같은 형태를 가지고 있어서 루트(root), 리프(leaf)라는 용어를 사용합니다.

그림1
[그림 1] 실제 나무(왼쪽)와 트리 자료구조의 모습(오른쪽)

오른쪽의 트리 자료구조에서 제일 최상 위의 '5'는 루트 노드(root node)라고 하며, 노드'5'와 노드'7'의 관계에서 노드'5'는 부모 노드(parent node), 노드'7'은 자식 노드(child node)라고 합니다. 또한 노드 '12'와 노드 '30'의 관계에서는 부모 노드는 노드'12'입니다. 자식이 없는 노드는 리프 노드(leaf node)라고 합니다. 그림 1에서는 '9', '30', '35', '20'이 리프 노드입니다.

7.2 트리 자료구조의 특징

트리는 노드를 균형 있게 가지는 것이 성능에 유리하기 때문에 기본 트리에서 변형된 B-트리, B+ 트리, R-트리, 레드 블랙 트리, AVL 트리 등 다양한 종류의 트리 자료구조가 있습니다.

균형을 이룬 트리는 자료를 정해진 방식에 따라서 분류하여 저장하기 때문에 시퀸스(일렬로)하게 자료를 저장하는 연결 리스트에 비해서 검색이 빠릅니다. 그렇지만 정해진 규칙에 따라서 자료를 삽입, 삭제 해야 되기 때문에 삽입과 삭제가 간단하지 않으며 구현이 복잡합니다.

7.3 map을 언제 사용해야 될까?

map은 많은 자료를 정렬하여 저장하고 있고 빠른 검색을 필요로 할 때 자주 사용합니다. 많은 자료를 빠르게 검색한다고 하는 부분은 앞 회에서 설명한 hash_map과 비슷합니다. 그러나 hash_map과 크게 다른 부분이 있습니다. map은 자료를 저장할 때 내부에서 자동으로 정렬을 하고, hash_map은 정렬하지 않는다라는 것입니다. 정렬이 필요하지 않는 곳에서 map을 사용하는 것은 불 필요한 낭비입니다.

map은 아래 조건일 때 사용하면 좋습니다.

1. 정렬해야 한다.
2. 많은 자료를 저장하고, 검색이 빨라야 한다
3. 빈번하게 삽입, 삭제하지 않는다.

7.4 map 사용 방법

가장 먼저 map의 헤더파일을 포함합니다.
#include 
보통 map을 사용하는 방법은 아래와 같습니다.
map< key 자료 type, value 자료 type > 변수 이름

map< int, int > map1;
value는 저장할 자료이고, key는 value를 가리키는 것입니다. 위에서는 key의 자료형 int, value 자료형 int인 map을 생성합니다.

앞에서 map은 자료를 저장할 때 정렬을 한다고 말했습니다. 정렬의 대상은 key를 대상으로 하며오름차순으로 정렬합니다. 그래서 내림차순으로 정렬하고 싶거나 key의 자료형이 기본형이 아닌 유저 정의형(class나 struct로 정의한 것)인 경우는 정렬 방법을 제공해야 합니다.

위에 생성한 map1은 오름차순으로 정렬하는데 이것을 내림차순으로 정렬하고 싶다면 아래와 같이 하면 됩니다.
map< key 자료 type, value 자료 type, 비교 함수 > 변수 이름

map< int, int, greater<int> > map1;
위에서 사용한 비교 함수 greater는 제가 따로 만든 것이 아니고 STL에 있는 템플릿입니다.

greater와 같은 것을 STL 알고리즘 이라고 하는데 이것들은 다음 시간에 자세하게 설명할 예정이니 여기서는 이런 것이 있다는 것만 아시면 됩니다.

다른 컨테이너와 같이 map도 동적 할당을 할 수 있습니다. 사용 방법은 앞서 소개한 컨테이너들과 비슷합니다.

앞 회의 hash_map과 비교를 하면 사용 방법이 거의 같다라는 것을 알 수 있습니다. 이후 소개하는 map의 멤버함수도 일부분만 제외하고는 hash_map과 같습니다. 이전에도 이야기 했지만 서로 다른 컨테이너가 사용방법이 서로 비슷하여 하나만 제대로 배우 나머지 것들도 배우기 쉽다라는 것이 STL의 장점 중의 하나입니다.

7.4.1 map의 주요 멤버들

멤버설명
begin첫 번째 원소의 랜덤 접근 반복자를 반환
clear저장하고 있는 모든 원소를 삭제
empty저장 하고 있는 요소가 없으면 true 반환
End마지막 원소 다음의(미 사용 영역) 반복자를 반환
erase특정 위치의 원소나 지정 범위의 원소들을 삭제
Findkey와 연관된 원소의 반복자 반환
insert원소 추가
lower_bound지정한 key의 요소를 가지고 있다면 해당 위치의 반복자를 반환
operator[]지정한 key 값으로 원소 추가 및 접근
rbegin역방향으로 첫 번째 원소의 반복자를 반환
rend역방향으로 마지막 원소 다음의 반복자를 반환
size원소의 개수를 반환
upper_bound지정한 key 요소를 가지고 있다면 해당 위치 다음 위치의 반복자 반환

[표 1] map의 주요 멤버들

7.4.2. 추가

map 에서는 자료를 추가 할 때 insert를 사용합니다.
원형 : 
pair <iterator, bool> insert( const value_type& _Val );
iterator insert( iterator _Where, const value_type& _Val );
template<class InputIterator> void insert( InputIterator _First, InputIterator _Last );
첫 번째 방식이 보통 가장 자주 사용하는 방식입니다.
map< int, int > map1;

// key는 1, value는 35를 추가.
map1.insert( map< int, int >::value_type(1, 35));

// 또는 STL의 pair를 사용하기도 합니다.
typedef pair < int, int > Itn_Pair;
map1.insert(  Int_Pair(2, 45) );
두 번째 방식으로는 특정 위치에 추가할 수 있습니다.
// 첫 번째 위치에 key 1, value 35를 추가
map1.insert( map1.begin(), map< int, int >::value_type(1, 35) );

// 또는
map1.insert( map1.begin(),  Int_Pair(2, 45) );
세 번째 방식으로는 지정한 반복자 구간에 있는 것들을 추가합니다.
map< int, int > map2;
// map1의 모든 요소를 map2에 추가.
map2.insert( map1.begin(), map1.end() );
map은 이미 있는 key 값을 추가할 수 없습니다(복수의 key 값을 사용하기 위해서는 multi_map을 사용해야 합니다). 가장 자주 사용하는 첫 번째 방식으로 추가하는 경우는 아래와 같은 방법으로 결과를 알 수 있습니다.
pair< map<int, int>::iterator, bool > Result;
Result = map1.insert( Int_Pair(1, 35));
만약 이미 key 값 1이 추가 되어 있었다면 insert 실패로 Result.second 는 false이며, 반대로 성공하였다면 true 입니다.

operator[]를 사용하여 추가하기

insert가 아닌 operator[]를 사용하여 추가할 수도 있습니다.
// key 10, value 80을 추가
map1[10] = 80;
7.4.3. 반복자 사용

다른 컨테이너와 같이 정 방향 반복자 begin(), end()와 역 방향 반복자 rbegin(), rend()를 지원합니다.

사용 방법은 다음과 같습니다.
// 정 방향으로 map1의 모든 요소의 value 출력
map< int, int >::iterator Iter_Pos;
for( Iter_Pos = map1.begin(); Iter_Pos != map1.end(); ++Iter_Pos)
{
   cout << Iter_Pos.second << endl;
}

// 역 방향으로 map1의 모든 요소의 value 출력
map< int, int >::reverse_iterator Iter_rPos;
for( Iter_rPos = map1.rbegin(); Iter_rPos != map1.rend(); ++Iter_rPos)
{
   cout << Iter_rPos.second << endl;
}
위에서 map을 정의할 때 비교함수를 사용할 수 있다고 했습니다. 만약 비교함수를 사용한 경우는 반복자를 정의할 때도 같은 비교함수를 사용해야 합니다.
map< int, int, greater<int> > map1;
map< int, int, greater<int> >::iterator Iter_Pos;
7.4.4. 검색

map에서 검색은 key 값을 대상으로 합니다. key와 같은 요소를 찾으면 그 요소의 반복자를 반환하고, 찾지 못한 경우에는 end()를 가리키는 반복자를 반환합니다.
원형 : 
iterator find( const Key& _Key );
const_iterator find( const Key& _Key ) const;
두 방식의 차이는 반환된 반복자가 const냐 아니냐는 차이입니다. 첫 번째 방식은 const가 아니므로 찾은 요소의 value를 변경할 수 있습니다(참고로 절대 key는 변경 불가입니다). 그러나 두 번째 방식은 value를 변경할 수 없습니다.
// key가 10인 요소 찾기.
map< int, int >::Iterator FindIter = map1.find( 10 );

// 찾았다면 value를 1000으로 변경
if( FindIter != map1.end() )
{
   FindIter->second = 1000;
}
7.4.5. 삭제

저장하고 있는 요소를 삭제할 때는 erase와 clear를 사용합니다. erase는 특정 요소를 삭제할 때 사용하고, clear는 모든 요소를 삭제할 때 사용합니다.

erase
원형 : 
iterator erase( iterator _Where );
iterator erase( iterator _First, iterator _Last );
size_type erase( const key_type& _Key );
첫 번째 방식은 특정 위치에 있는 요소를 삭제합니다.
// 두 번째 위치의 요소 삭제.
map1.erase( ++map1.begin() );
두 번째 방식은 지정한 구역에 있는 요소들을 삭제합니다.
// map1의 처음과 마지막에 있는 모든 요소 삭제
map1.erase( map1.begin(), map1.end() );
세 번째 방식은 지정한 키와 같은 요소를 삭제합니다.
// key가 10인 요소 삭제.
map1.erase( 10 );
첫 번째와 두 번째 방식에서는 삭제하는 요소의 다음을 가리키는 반복자를 반환하고(C++ 표준에서는 반환하지 않습니다만 Microsoft의 Visual C++에서는 반환합니다), 세 번째 방식은 삭제된 개수를 반환합니다. map에서는 세 번째 방식으로 삭제를 하는 경우 정말 삭제가 되었다면 무조건 1이지만, multi_map에서는 삭제한 개수만큼의 숫자가 나옵니다.

clear

map의 모든 요소를 삭제할 때는 clear를 사용합니다.
map1.clear();
이것으로 map에서 자주 사용하는 멤버들에 대한 설명은 끝났습니다. [표 1]에 나와 있는 멤버들 중 사용 방법이 간단한 것은 따로 설명하지 않으니 [리스트 1]의 코드를 봐 주세요.

[리스트 1] 정렬된 아이템 리스트 출력
#include <map>
#include <string>
#include <iostream>

using namespace std;

struct Item
{
  char Name[32];  // 이름
  char Kind;  // 종류
  int BuyMoney;  // 구입 가격
  int SkillCd;  // 스킬 코드
};

int main()
{
  map< char*, Item > Items;
  map< char*, Item >::iterator IterPos;
  typedef pair< char*, Item > ItemPair;

  Item Item1;
  strncpy( Item1.Name, "긴칼", 32 );
  Item1.Kind = 1;    Item1.BuyMoney = 200;  Item1.SkillCd = 0;

  Item Item2;
  strncpy( Item2.Name, "성스러운 방패", 32 );
  Item2.Kind = 2;    Item2.BuyMoney = 1000;  Item2.SkillCd = 4;

  Item Item3;
  strncpy( Item3.Name, "해머", 32 );
  Item3.Kind = 1;    Item3.BuyMoney = 500;  Item3.SkillCd = 0;

  // Items에 아이템 추가
  Items.insert( map< char*, Item >::value_type(Item2.Name, Item2) );
  Items.insert( ItemPair(Item1.Name, Item1) );

  // Items가 비어 있지않다면
  if( false == Items.empty() )
  {
    cout << "저장된 아이템 개수- " << Items.size() << endl;
  }

  for( IterPos = Items.begin(); IterPos != Items.end(); ++IterPos )
  {
    cout << "이름: " << IterPos->first << ", 가격: " << IterPos->second.BuyMoney << endl;
  }

  IterPos = Items.find("긴칼");
  if( IterPos == Items.end() )   {
    cout << "아이템'긴칼'이 없습니다." << endl;
  }
  cout << endl;

  cout << "올림차순으로 정렬되어있는 map(Key 자료형으로string 사용)" << endl;
  
  map< string, Item, less<string> > Items2;
  map< string, Item, less<string> >::iterator IterPos2;
  
  Items2.insert( map< string, Item >::value_type(Item2.Name, Item2) );
  Items2.insert( ItemPair(Item1.Name, Item1) );
  // operator[]를 사용하여 저장
  Items2[Item3.Name] = Item3;

   for( IterPos2 = Items2.begin(); IterPos2 != Items2.end(); ++IterPos2 )
  {
    cout << "이름: " << IterPos2->first << ", 가격: " << IterPos2->second.BuyMoney << endl;
  }
  cout << endl;

  cout << "해머의 가격은 얼마? ";
  IterPos2 = Items2.find("해머");
  if( IterPos2 != Items2.end() )   {
    cout << IterPos2->second.BuyMoney << endl;
  }
  else {
    cout << "해머는 없습니다" << endl;
  }
  cout << endl;

  // 아이템 "긴칼"을 삭제한다.
  IterPos2 = Items2.find("긴칼");
  if( IterPos2 != Items2.end() )   {
    Items2.erase( IterPos2 );
  }

  cout << "Items2에 있는 아이템 개수: " << Items2.size() << endl;

  return 0;
}
결과

그림2

[리스트 1]의 Items에서 '긴칼'을 검색을 하면 찾을 수가 없습니다. 이유는 key의 자료형으로 char*을 사용했기 때문입니다. 그래서 Items2에서는 STL의 문자열 라이브러리인 string을 사용하였습니다. String에 대해서는 다음 기회에 설명할 예정이니 문자열을 처리하는 라이브러리라고 알고 계시면 됩니다.

이것으로 map에 대한 설명이 끝났습니다. 이전 회의 hash_map과 비슷한 부분이 많아서 hash_map에 대한 글을 보셨던 분들은 쉽게 따라왔으리라 생각합니다. 그리고 map과 hash_map에 대하여 잘못 알고 있어서 정렬이 필요하지 않은 곳에서 map을 사용하는 경우가 있는데 조심하시기 바랍니다. 제가 미쳐 설명하지 않은 부분에 대해서는 MSDN에 있는 map 설명을 참조하시기를 바랍니다(http://msdn.microsoft.com/ko-kr/library/xdayte4c.aspx).

과제

1. [리스트 1]은 아이템 이름을 key 값으로 사용하였습니다. 이번에는 아이템 가격을 Key 값으로 사용하여 아이템을 저장하고 내림차순으로 출력해 보세요.

앞서 중복된 key를 사용할 수 있는 multi_map 이라는 것이 있다고 이야기 했습니다. 이번 과제는 multi_map을 사용해야 합니다. multi_map에 관한 설명은 아래 링크의 글을 참조하세요. http://msdn.microsoft.com/ko-kr/library/1y9w8dz4.aspx


[출처 : http://www.hanb.co.kr/network/view.html?bi_id=1618]
제공 : 한빛 네트워크
저자 : 최흥배
이전기사 : About STL을 보시는 분은 대부분 아직 STL을 잘 모르는 분들이라고 생각합니다. 제가 일하고 있는 게임업계는 주력 언어가 C++입니다. 그래서 취업 사이트에 올라온 프로그래머 채용 공고를 보면 필수 조건에 거의 대부분이 C++와 STL 사용 가능이 들어가 있습니다. 게임 업계뿐 아니라 C++을 사용하여 프로그래밍하는 곳이라면 대부분 C++과 STL을 사용하여 프로그램을 만들 수 있는 실력을 필요로 합니다.
C++ 언어를 배우고 사용하는 프로그래머라면 STL을 배우면 좋고, 특히 게임 프로그래머가 되실 분들은 STL을 꼭 사용할 줄 알아야 됩니다.
작년 여름부터 About STL을 쓰기 시작하여 지금은 2009년이 되었습니다. About STL 집필 계획으로는 이제 반 정도 도달한 것 같습니다. 앞으로 남은 반도 STL을 습득하는데 도움이 되도록 저도 최대한 노력할 테니 2009년에는 STL을 꼭 마스터하기를 바랍니다.

6.1 시퀸스 컨테이너와 연관 컨테이너

이전 회까지는 STL의 컨테이너에 대해서 설명했었습니다. STL 컨테이너는 크게 시퀸스 컨테이너와 연관 컨테이너로 나눕니다.
시퀸스 컨테이너는 vector, list, deque와 같이 순서 있게 자료를 보관합니다.
연관 컨테이너는 어떠한 Key와 짝을 이루어 자료를 보관합니다. 그래서 자료를 넣고, 빼고, 찾을 때는 Key가 필요합니다.

그림1
[그림 1] 시퀸스 컨테이너와 연관 컨테이너

시퀸스 컨테이너는 많지 않은 자료를 보관하고 검색 속도가 중요한 경우에 사용하고, 연관 컨테이너는 대량의 자료를 보관하고 검색을 빠르게 하고 싶을 때 사용합니다. 제가 만드는 온라인 게임 서버에서는 보통 접속한 유저들의 정보를 보관할 때 가장 많이 사용합니다.

6.2 연관 컨테이너로는 무엇이 있을까요?

연관 컨테이너로 map, set, hash_map, hash_set이 있습니다. 이것들은 Key로 사용하는 값이 중복되지 않은 때 사용합니다. 만약 중복되는 key를 사용할 때는 컨테이너의 앞에 'multi'를 붙인 multi_map, multi_set, hash_multimap, hash_multiset을 사용합니다. Key의 중복 허용 여부만 다를 뿐 사용방법은 같습니다.

6.2.1 map, set 과 hash_map, hash_set의 차이는?

가장 쉽게 알 수 있는 큰 차이는 이름 앞에 'hash'라는 단어가 있냐 없냐의 차이겠죠.^^
네, 'hash'라는 단어가 정말 큰 차이입니다.
map과 set 컨테이너는 자료를 정렬하여 저장합니다. 그래서 반복자로 저장된 데이터를 순회할 때 넣은 순서로 순회하지 않고 정렬된 순서대로 순회합니다. hash_map, hash_set은 정렬 하지 않으며 자료를 저장합니다. 또 hash라는 자료구조를 사용함으로 검색 속도가 map, set에 비해 빠릅니다.

map, set과 hash_map, hash_set 중 어느 것을 사용할지 생각할 때는
map, set의 사용하는 경우 : 정렬된 상태로 자료 저장을 하고 싶을 때.
hash_map, hash_set : 정렬이 필요 없으며 빠른 검색을 원할 때.

를 가장 큰 조건으로 보면 좋습니다.

6.2.2 hash_map, hash_set은 표준은 아닙니다.

위에 열거한 연관 컨테이너 중 map과 set은 STL 표준 컨테이너지만 hash_map, hash_set은 표준이 아닙니다. 그래서 보통 STL 관련 책을 보시면 hash_map과 hash_set에 대한 설명은 없습니다. hash_map, hash_set을 쓰려면 라이브러리를 설치해야 할까요? 그럴 필요는 없습니다. STL 표준은 아니지만 오래되지 않은 C++ 컴파일러에서는 대부분 지원합니다. 윈도우에서는 Visual Studio.NET의 모든 버전에서 지원합니다.
STL 표준도 아닌 hash_map을 설명하려는 이유는 대부분 C++ 컴파일러에서 지원하고, 새로운 C++ 표준에서는 정식으로 STL에 들어갈 예정이며 현업에서 프로그래밍할 때 아주 유용하게 사용하는 컨테이너이기 때문입니다.

[참고]
2010년 이내에 새로운 C++ 표준이 만들어질 예정인데 표준이 공표되기 전에 TR1으로 일부 공개하고 있습니다. TR1에서는 hash_map, hash_set과 거의 같은 컨테이너인 unordered_map, unordered_set이 준비되어 있습니다. hash_map, hash_set과 이름만 다를 뿐 컨테이너의 자료구조나 사용방법이 거의 같습니다.
그래서 hash_map, hash_set 사용법을 익히며 자동으로 unordered_map, unordered_set도 익히게 됩니다.

6.3 hash_map의 자료구조

hash_map의 자료구조는 '해시 테이블'입니다.
아래의 [그림 2]에 나와 있듯이 해시 테이블에 자료를 저장할 때는 Key 값을 해시함수에 대입하여 버킷 번호가 나오면 그 버킷의 빈 슬롯에 자료를 저장합니다.
Key 값을 해시 함수에 입력하여 나오는 버킷 번호에 자료를 넣으므로 많은 자료를 저장해도 삽입, 삭제, 검색 속도가 거의 일정합니다.

그림2
[그림 2] 해시 테이블에 자료 넣기

해시 테이블에 대한 설명은 간단하지 않고 이 글에서는 hash_map 사용법을 간단하게 설명하고 마치려 합니다. 좀 더 자세하게 알고 싶은 분들은 아래의 참고 항목을 꼭 봐 주세요.

[참고] 해시 테이블 설명
1. http://internet512.chonbuk.ac.kr/datastructure/hash/hash3.htm
2. 좋은 프로그램을 만드는 핵심 원리 25가지(한빛미디어)

6.4 hash_map을 사용할 때와 사용하지 않을 때

이전 연재에서 설명한 STL 컨테이너의 장단점은 컨테이너의 자료구조를 보면 알 수 있습니다. hash_map은 해시 테이블을 자료구조로 사용하므로 해시 테이블에 대해 알면 장단점을 파악할 수 있습니다. 해시 테이블은 많은 자료를 저장하고 있어도 검색이 빠릅니다. 그러나 저장한 자료가 적을 때는 메모리 낭비와 검색 시 오버헤드가 생깁니다.
Key 값을 해시 함수에 넣어 알맞은 버킷 번호를 알아 내는 것은 마법 같은 것이 아닙니다. 그러니 hash_map은 해시 테이블을 사용하므로 검색이 빠르다라는 것만 생각하고 무분별하게 hash_map을 사용하면 안됩니다. 컨테이너에 추가나 삭제를 하는 것은 list나 vector, deque가 hash_map보다 빠릅니다. 또 적은 요소를 저장하고 검색할 때는 vector나 list가 훨씬 빠를 수 있습니다. 수천의 자료를 저장하여 검색을 하는 경우에 hash_map을 사용하는 것이 좋습니다.

hash_map을 사용하는 경우
1. 많은 자료를 저장하고, 검색 속도가 빨라야 한다.
2. 너무 빈번하게 자료를 삽입, 삭제 하지 않는다.

6.5 Hash_map 사용방법

STL의 다른 컨테이너와 같이 사용하려면 먼저 헤더 파일과 namespace를 선언해야 합니다. 그러나 여기서 주의할 점은 앞서 이야기 했듯이 hash_map은 표준이 아니므로 표준 STL의 namespace와 다른 이름을 사용하므로 namespace 선언할 때 실수하지 않게 조심하세요.
hash_map 헤더파일을 포함합니다.
#include <hash_map>
hash_map이 속한 namespace는 표준 STL과 다른 'stdext'입니다.
using namespace stdext;
hash_map 선언은 아래와 같습니다.
hash_map< Key 자료 type, Value 자료 type > 변수 이름
위에서는 Value는 저장할 데이터이고, Key는 Value와 가리키는 데이터입니다.

Key는 int, Value는 float를 사용한다면 아래와 같습니다.
hash_map< int, float > hash1;
다른 컨테이너와 같이 동적 할당을 할 수 있습니다.
hash_map< key 자료 type, Value 자료 type >* 변수 이름 = new hash_map< key 자료 type, Value 자료 type >;
hash_map< int, float >* hash1 = new hash_map< int, float >;
hash_map은 Key와 Value가 짝을 이뤄야 하므로 hash_map을 처음 보는 분들은 이전의 시퀸스 컨테이너와 다르게 좀 복잡하게 보일 것입니다. 그러나 사용이 어려운 것은 아니니 잘 따라와 주세요.

6.5.1 hash_map의 주요 멤버들

멤버 설명
begin 첫 번째 원소의 랜덤 접근 반복자를 반환
clear 저장한 모든 원소를 삭제
empty 저장한 요소가 없으면 true 반환
end 마지막 원소 다음의(미 사용 영역) 반복자를 반환
erase 특정 위치의 원소나 지정 범위의 원소들을 삭제
find Key와 연관된 원소의 반복자 반환
insert 원소 추가
lower_bound 지정한 Key의 요소가 있다면 해당 위치의 반복자를 반환
rbegin 역방향으로 첫 번째 원소의 반복자를 반환
rend 역방향으로 마지막 원소 다음의 반복자를 반환
size 원소의 개수를 반환
upper_bound 지정한 Key 요소가 있다면 해당 위치 다음 위치의 반복자 반환


hash_map 컨테이너를 사용할 때는 거의 대부분 추가, 삭제, 검색 이렇게 3가지를 사용합니다. 핵심 기능인 만큼 아래에 좀 더 자세하게 설명하고, 다른 컨테이너는 앞서 설명한 것과 사용방법이 같으므로 예제 코드로 보여 드리겠습니다.

6.5.2 추가

insert

hash_map 에서는 자료를 추가 할 때 insert를 사용합니다.
원형 : 
pair <iterator, bool> insert( const value_type& _Val );
iterator insert( iterator _Where, const value_type& _Val );
template<class InputIterator> void insert( InputIterator _First, InputIterator _Last );
insert를 사용하는 세 가지 방법 중 첫 번째 방식으로 Key 타입은 int, Value 타입은 float를 추가한다면
hash_map<int, float> hashmap1, hashmap2;

// Key는 10, Value는 45.6f를 추가.
hashmap1.insert(hash_map<int, float>::value_type(10, 45.6f));
두 번째 방식으로는 특정 위치에 추가할 수 있습니다.
// 첫 번째 위치에 key 11, Value 50.2f를 추가
hashmap1.insert(hashmap1.begin(), hash_map<int, float>::value_type(11, 50.2f));
세 번째 방식으로는 지정한 반복자 구간에 있는 것들을 추가합니다.
// hashmap1의 모든 요소를 hashmap2에 추가.
hashmap2.insert( hashmap1.begin(), hashmap1.end() );
6.5.3 삭제

erase
원형 : 
iterator erase( iterator _Where );
iterator erase( iterator _First, iterator _Last );
size_type erase( const key_type& _Key );
첫 번째 방식은 특정 위치에 있는 요소를 삭제합니다.
// 첫 번째 위치의 요소 삭제.
hashmap1.erase( hashmap1.begin() );
두 번째 방식은 지정한 구역에 있는 요소들을 삭제합니다.
// hashmap1의 처음과 마지막에 있는 모든 요소 삭제
hashmap1.erase( hashmap1.begin(), hashmap1.end() );
세 번째 방식은 지정한 키와 같은 요소를 삭제합니다.
// Key가 11인 요소 삭제.
hashmap1.erase( 11 );
첫 번째와 두 번째 방식의 반환 값으로는 삭제된 요소의 다음의 것을 가리키는 반복자이며 세 번째 방식은 삭제된 개수를 반환합니다.

6.5.4 검색

hahs_map에서 검색은 Key를 사용하여 같은 Key를 가지고 있는 요소를 찾습니다.
Key와 같은 요소를 찾으면 그 요소의 반복자를 반환하고, 찾지 못한 경우에는 end()를 가리키는 반복자를 반환합니다.
원형 : 
iterator find( const Key& _Key );
const_iterator find( const Key& _Key ) const;
방식은 두 가지지만 사용법은 같습니다. 차이는 반환된 반복자가 const냐 아니냐의 차이입니다. 참고로 첫 번째 방식은 const가 아니므로 찾은 요소의 Value를 변경할 수 있습니다(참고로 Key는 변경 불가입니다). 두 번째 방식은 Value도 변경할 수 없습니다.
// Key가 10인 요소 찾기.
hash_map<int, float>::Iterator FindIter = hashmap1.find( 10 );

// 찾았다면 Value를 290.44로 변경
If( FindIter != hashmap1.end() )
{
   FindIter->second = 290.44f;
}
begin, clear, count, empty, end, rbegin, rend, size는 앞서 말 했듯이 다른 컨테이너와 사용방법이 비슷하므로 아래 예제 코드를 통해서 사용법을 보여 드리겠습니다.

[리스트 1] hash_map을 사용한 유저 관리
#include <iostream>
#include <hash_map>
using namespace std;
using namespace stdext;

// 게임 캐릭터
struct GameCharacter
{
  // 아래의 인자를 가지는 생성자를 정의한 경우는
  // 꼭 기본 생성자를 정의해야 컨테이너에서 사용할 수 있다.
  GameCharacter() { }

  GameCharacter( int CharCd, int Level, int Money )
  {
    _CharCd = CharCd;
    _Level = Level;
    _Money = Money;
  }
  int _CharCd;    //  캐릭터 코드
  int _Level;    // 레벨
  int _Money;    // 돈
};

void main()
{
  hash_map<int, GameCharacter> Characters;

  GameCharacter Character1(12, 7, 1000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(12, Character1));

  GameCharacter Character2(15, 20, 111000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(15, Character2));

  GameCharacter Character3(200, 34, 3345000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(200, Character3));
  
  // iterator와 begin, end 사용
  // 저장한 요소를 정방향으로 순회
  hash_map<int, GameCharacter>::iterator Iter1;
  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl; 

  // rbegin, rend 사용
  // 저장한 요소의 역방향으로순회
  hash_map<int, GameCharacter>::reverse_iterator RIter;
  for( RIter = Characters.rbegin(); RIter != Characters.rend(); ++RIter )
  {
    cout << "캐릭터 코드 : " << RIter->second._CharCd << " |   레벨 : " << RIter->second._Level 
      << "|  가진 돈 : " <<  RIter->second._Money << endl; 
  }
  cout << endl << endl;
 
  // Characters에 저장한 요소 수
  int CharacterCount = Characters.size();

  // 검색. 
  // 캐릭터 코드 15인 캐릭터를 찾는다.
  hash_map<int, GameCharacter>::iterator FindIter = Characters.find(15);
  // 찾지 못했다면 FindIter은 end 위치의 반복자가 반환된다.
  if( Characters.end() == FindIter )
  {
    cout << "캐릭터 코드가 20인 캐릭터가 없습니다" << endl;
  }
  else
  {
    cout << "캐릭터 코드가 15인 캐릭터를 찾았습니다." << endl;
    cout << "캐릭터 코드 : " << FindIter->second._CharCd << " |   레벨 : " << FindIter->second._Level 
      << "|  가진 돈 : " <<  FindIter->second._Money << endl;
  }
  cout << endl;

  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl << endl;
  
  // 삭제
  // 캐릭터 코드가 15인 캐릭터를 삭제한다.
  Characters.erase( 15 );
  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl << endl;

  // 모든 캐릭터를 삭제한다.
  Characters.erase( Characters.begin(), Characters.end() );

  // Characters 공백 조사
  if( Characters.empty() )
  {
    cout << "Characters는 비어 있습니다." << endl;
  }
}
결과

결과1

6.5.5 lower_bound와 upper_bound

hash_map에 저장한 요소 중에서 Key 값으로 해당 요소의 시작 위치를 얻을 때 사용하는 멤버들입니다. Key 값의 비교는 크기가 아닌 저장 되어 있는 요소의 순서입니다. 23, 4, 5, 18, 14, 30 이라는 순서로 Key 값을 가진 요소가 저장되어 있으며 Key 값 18과 같거나 큰 것을 찾으면 18, 14, 30이 됩니다.

lower_bound

Key가 있다면 해당 위치의 반복자를 반환합니다.
원형 : 
iterator lower_bound( const Key& _Key );
const_iterator lower_bound( const Key& _Key ) const;
upper_bound

Key가 있다면 그 요소 다음 위치의 반복자를 반환합니다.
원형 : 
iterator lower_bound( const Key& _Key );
const_iterator lower_bound( const Key& _Key ) const;
lower_bound와 upper_bound는 hahs_map에 저장된 요소를 일부분씩 나누어 처리를 할 때 유용합니다. 예를 들면 hash_map에 3,000개의 게임 캐릭터 정보를 저장되어 있으며 이것을 100개씩 나누어서 처리하고 싶을 때 사용하면 좋습니다.

[리스트 2] lower_bound와 upper_bound 사용 예
#include <iostream>
#include <hash_map>
using namespace std;
using namespace stdext;

// 게임 캐릭터
struct GameCharacter
{
  GameCharacter() { }

  GameCharacter( int CharCd, int Level, int Money )
  {
    _CharCd = CharCd;
    _Level = Level;
    _Money = Money;
  }
  int _CharCd;    //  캐릭터코드
  int _Level;    // 레벨
  int _Money;    // 돈
};

void main()
{
  hash_map<int, GameCharacter> Characters;

  GameCharacter Character1(12, 7, 1000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(12, Character1));

  GameCharacter Character2(15, 20, 111000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(15, Character2));

  GameCharacter Character3(7, 34, 3345000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(7, Character3));

  GameCharacter Character4(14, 12, 112200 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(14, Character4));

  GameCharacter Character5(25, 3, 5000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(25, Character5));

  hash_map<int, GameCharacter>::iterator Iter1;
  cout << "저장한 캐릭터 리스트" << endl;
  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl;

  cout << "lower_bound(14)" <<endl;
  hash_map<int, GameCharacter>::iterator Iter = Characters.lower_bound(14);
  while( Iter != Characters.end() )
  {
    cout << "캐릭터 코드 : " << Iter->second._CharCd << " |   레벨 : " << Iter->second._Level 
      << "|  가진 돈 : " <<  Iter->second._Money << endl;

    ++Iter;
  }
  cout << endl;

  cout << "upper_bound(7)" <<endl;
  Iter = Characters.upper_bound(7);
  while( Iter != Characters.end() )
  {
    cout << "캐릭터 코드 : " << Iter->second._CharCd << " |   레벨 : " << Iter->second._Level 
      << "|  가진 돈 : " <<  Iter->second._Money << endl;

    ++Iter;
  }
}
결과

결과2

이것으로 hash_map의 주요 사용법에 대한 설명이 끝났습니다.
위에 설명한 것들만 알고 있으면 hash_map을 사용하는데 문제가 없을 것입니다.
위에 설명한 것들 이외의 hash_map의 멤버들에 대해서 알고 싶으며 마이크로소프트의 MSDN 사이트에 있는 것을 참고하세요
http://msdn.microsoft.com/en-us/library/h80zf4bx(VS.80).aspx

[참고]
Visual C++의 hash_map의 성능에 대해서 Visual C++에 있는 hash_map은 다른 컴파일에서 구현한 것보다 꽤 느리다라는 말이 있습니다.
(관련 글은 여기서 참고하세요. http://minjang.egloos.com/1983788 http://junyoung.tistory.com/1)

얼마나 느린지 테스트했습니다.
http://blog.naver.com/jacking75/140062720030
제가 조사한 것은 Windows 플랫폼에서 VC++에서 제공한 라이브러리로 테스트한 것입니다.
결과를 보면 hash_map이 map보다 빠르지도 않고 특히 hash_map과 같은 자료구조를 사용하는 컨테이너로 마이크로소프트사에서 만든 CAtlMap에 비해 속도가 아주 느립니다.
성능이 중요한 곳에 hash_map을 사용한다면 VC++에 있는 것을 사용하지 말고 자체적으로 잘 만들어진 hash 함수를 사용하거나 C++ 오픈 소스 라이브러리인 boost에 있는 unordered_map을 사용하는 것이 좋을 것 같습니다. Windows 플랫폼에서만 사용한다면 CAtlMap을 사용하는 것도 좋습니다.


[출처 : http://www.hanb.co.kr/network/view.html?bi_id=1617]
제공 : 한빛 네트워크
저자 : 최흥배
이전기사 :

5.5.3. deque 실습 예제

다음은 deque에서 가장 자주 사용하는 멤버들을 사용하는 전체 코드입니다.

[리스트 1]
#include 
#include 

using namespace std;


// 서버/ 클라이언트간에 주고 받는 패킷
struct Packet
{
	unsigned short Index; // 패킷 인덱스
	unsigned short BodySize; // 패킷 보디(실제 데이터)의 크기
	char	       acBodyData[100]; // 패킷의 데이터
};


int main() 
{
	Packet Pkt1;
	Pkt1.Index = 1;		Pkt1.BodySize = 10;

	Packet Pkt2;
	Pkt2.Index = 2;		Pkt2.BodySize = 12;

	Packet Pkt3;
	Pkt3.Index = 3;		Pkt3.BodySize = 14;


	deque< Packet > ReceivePackets;


	// 뒤에 추가
	ReceivePackets.push_back( Pkt2 );
	ReceivePackets.push_back( Pkt3 );

	// 앞에 추가
	ReceivePackets.push_front( Pkt1 );

	
	// 저장된 패킷 정보 출력
	for( deque< Packet >::iterator iterPos = ReceivePackets.begin();
		iterPos != ReceivePackets.end();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index << endl;
		cout << "패킷 바디 크기: " << iterPos->BodySize << endl;
	}

	cout << endl << "역방향으로 출력" << endl;
	for( deque< Packet >::reverse_iterator iterPos = ReceivePackets.rbegin();
		iterPos != ReceivePackets.rend();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index << endl;
		cout << "패킷 바디 크기: " << iterPos->BodySize << endl;
	}

	cout << endl << "배열 방식으로 접근" << endl;
	// 저장된 총 패킷 수
	int ReceivePacketCount = ReceivePackets.size();
	cout << "총 패킷 수: " << ReceivePacketCount << endl;
	for( int i = 0; i < ReceivePacketCount; ++i )
	{
		cout << "패킷 인덱스: " << ReceivePackets[i].Index << endl;
		cout << "패킷 바디 크기: " << ReceivePackets[i].BodySize << endl;
	}


	// 첫 번째, 마지막 위치에 있는 패킷
	Packet& FirstPacket = ReceivePackets.front();
	cout << "첫 번째 패킷의 인덱스: " << FirstPacket.Index << endl;

	Packet& LastPacket = ReceivePackets.back();
	cout << "마지막 패킷의 인덱스: " << LastPacket.Index << endl;

	// at을 사용하여 두 번째 패킷
	Packet& PacketAt = ReceivePackets.at(1);
	cout << "두 번째 패킷의 인덱스: " << PacketAt.Index << endl;


	// 첫 번째 패킷 삭제
	ReceivePackets.pop_front();
	cout << "첫 번째 패킷의 인덱스: " << ReceivePackets[0].Index << endl;

	// 마지막패킷삭제
	ReceivePackets.pop_back();
	LastPacket = ReceivePackets.back();
	cout << "마지막 패킷의 인덱스: " << LastPacket.Index << endl;

	// 모든 패킷을 삭제
	if( false == ReceivePackets.empty() )
	{
		cout << "모든 패킷을 삭제합니다." << endl;
		ReceivePackets.clear();
	}
}
결과

null

5.5.3.1. insert 멤버

deque의 insert는 vector의 insert와 사용 방법이 같습니다. 지정된 위치에 삽입, 지정된 위치에 지정된 개수만큼 삽입, 지정된 위치에 반복자 구간 안에 있는 것들을 삽입이 있습니다. vector와 같이 insert는 삽입 되는 위치 이후에는 있는 모든 원소들이 뒤로 이동을 합니다. 그리고 insert의 성능은 vector 보다도 더 좋지 않습니다.
원형 : iterator insert( iterator _Where, const Type& _Val );
      void insert( iterator _Where, size_type _Count, const Type& _Val );
      template void insert( iterator _Where, InputIterator _First, 
                                            InputIterator _Last );

[그림 6] deque의 insert 처리 과정

지정한 위치에 데이터 삽입. 아래는 300을 첫 번째 위치에 삽입합니다.
deque< int >::iterator iterInsertPos = deque1.begin();
deque1.insert( iterInsertPos, 300 );
지정한 위치에 데이터를 횟수만큼 삽입. 아래는 두 번째 위치에 10을 3번 추가합니다.
iterInsertPos = deque1.begin();
++iterInsertPos;
deque1.insert( iterInsertPos, 3, 10 );
지정한 위치에 반복자의 시작과 끝 안에 있는 원소를 삽입. 아래는 두 번째 위치에 deque2의 처음과 끝에 있는 원소를 삽입합니다.
deque< int > deque2;
deuqe2.push_back( 20 );
deuqe2.push_back( 30 );
deuqe2.push_back( 40 );

iterInsertPos = deque1.begin();
deque1.insert( ++iterInsertPos, deque2.begin(),deque2.end() );

5.5.3.2. erase 멤버

지정한 위치에 있는 원소를 삭제, 지정한 범위 안에 있는 원소를 삭제합니다. vector와 사용법이 같고 또 erase를 하는 위치 이후의 모든 원소들이 앞으로 이동하는 것도 같습니다.
원형 : iterator erase( iterator _Where );
      iterator erase( iterator _First, iterator _Last );

[그림 7] deque의 erase 처리 과정

지정한 위치의 요소를 삭제. 아래는 첫 번째 요소를 삭제하는 코드입니다.
deque1.erase( deque1.begin() );
지정한 반복자 구간 안의 원소를 삭제합니다. 아래는 deque1의 첫 번째 요소에서 마지막까지 모두 삭제합니다.
deque1.erase( deque1.begin(), deque1.end() );
아래는 insert와 erase 사용 예입니다.

[리스트 2] Insert와 erase
#include 
#include 

using namespace std;


int main()
{
	Packet Pkt1;
	Pkt1.Index = 1;		Pkt1.BodySize = 10;

	Packet Pkt2;
	Pkt2.Index = 2;		Pkt2.BodySize = 12;

	Packet Pkt3;
	Pkt3.Index = 3;		Pkt3.BodySize = 14;

	Packet Pkt4;
	Pkt4.Index = 4;		Pkt4.BodySize = 16;

	deque< Packet > ReceivePackets;
	ReceivePackets.push_back( Pkt1 );
	ReceivePackets.push_back( Pkt2 );
	ReceivePackets.push_back( Pkt3 );

	cout << "< insert >" << endl;

	// 첫 번째 위치에 Pkt3을 삽입
	cout << "insert 1" << endl;
	ReceivePackets.insert( ReceivePackets.begin(), Pkt3 );

	for( deque< Packet >::iterator iterPos = ReceivePackets.begin();
		iterPos != ReceivePackets.end();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index;
		cout << "        패킷 바디 크기: " << iterPos->BodySize << endl;
	}

	// 두 번째 위치에 Pkt4를 2개 삽입
	cout << endl << "insert 2" << endl;
	ReceivePackets.insert( ++ReceivePackets.begin(), 2, Pkt4 );
	for( deque< Packet >::iterator iterPos = ReceivePackets.begin();
		iterPos != ReceivePackets.end();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index;
		cout << "        패킷 바디 크기: " << iterPos->BodySize << endl;
	}


	deque< Packet > ReceivePackets2;
	ReceivePackets2.push_back( Pkt3 );
	ReceivePackets2.push_back( Pkt4 );
	ReceivePackets2.push_back( Pkt1 );

	// ReceivePackets2의 모든 것을 ReceivePackets의 첫 번째 위치에 삽입
	cout << endl << "insert 3" << endl;
	ReceivePackets.insert( ReceivePackets.begin(), ReceivePackets2.begin(), 
                                  ReceivePackets2.end() );
	for( deque< Packet >::iterator iterPos = ReceivePackets.begin();
		iterPos != ReceivePackets.end();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index;
		cout << "        패킷 바디 크기: " << iterPos->BodySize << endl;
	}


	cout << endl << "< erase >" << endl;	
	// 두 번째 원소를 삭제한다.
	cout << "erase 1" << endl;
	ReceivePackets.erase( ++ReceivePackets.begin() );
	for( deque< Packet >::iterator iterPos = ReceivePackets.begin();
		iterPos != ReceivePackets.end();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index;
		cout << "        패킷 바디 크기: " << iterPos->BodySize << endl;
	}

	// 두 번째 이후부터 모두 삭제한다.
	cout << endl << "erase 2" << endl;
	ReceivePackets.erase( ++ReceivePackets.begin(), ReceivePackets.end() );
	for( deque< Packet >::iterator iterPos = ReceivePackets.begin();
		iterPos != ReceivePackets.end();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index;
		cout << "        패킷 바디 크기: " << iterPos->BodySize << endl;
	}
}
결과

5.5.1.4 assign

vector의 assign과 같이 deque의 assign도 컨테이너를 특정 데이터로 채울 때 사용합니다. 특정 값이나 다른 deque의 특정 영역(반복자로 가리키는)에 있는 데이터로 채울 수 있습니다. assign을 사용하는 deque에 데이터가 있다면 이를 덮어쓰면서 채웁니다.
원형 : void assign( size_type _Count, const Type& _Val );
template void assign( InputIterator _First, InputIterator _Last );

[그림 8] deque의 assign

지정 데이터를 지정 개수만큼 채웁니다. 숫자 7를 7개 채웁니다.
deque1.assign( 7, 7 );
다른 deque의 반복자로 지정한 영역으로 채웁니다.
deque1.erase( deque1.begin(), deque1.end() );

5.5.1.5 swap

deque1과 deque2가 있을 때 두 deque에 저장한 데이터를 서로 맞바꿀 때 사용합니다. swap 원형은 아래와 같습니다.
원형 : void swap( deque& _Right );
friend void swap( deque& _Left, deque& _Right )
template void swap(
       deque< Type, Allocator>& _Left, deque< Type, Allocator>& _Right );
deque A와 B를 swap하는 모습을 그림으로 나타내면 아래와 같습니다.


[그림 9] deque의 swap

아래는 deque1과 deque2를 swap하는 방법을 두 가지로 나타낸 코드입니다.
deque1.swap( deque2 );
swap(deque1, deque2 );
[리스트 3] assign, swap
#include 
#include 

using namespace std;

int main()
{
	deque< int > deque1;

	cout << "assign 1" << endl;
	deque1.assign( 7, 7 );
	for( deque< int >::iterator iterPos = deque1.begin();
		iterPos != deque1.end();
		++iterPos )
	{
		cout << "deque 1 : " << *iterPos << endl;
	}

	cout << endl << "assign 2" << endl;
	deque< int > deque2;
	deque2.assign( deque1.begin(), deque1.end() );
	for( deque< int >::iterator iterPos = deque2.begin();
		iterPos != deque2.end();
		++iterPos )
	{
		cout << "deque 2 : " << *iterPos << endl;
	}

	// swap
	deque< int > deque3;
	deque3.push_back(10);
	deque3.push_back(20);
	deque3.push_back(30);

	cout << endl << "swap" << endl;
	deque3.swap( deque1 );
	for( deque< int >::iterator iterPos = deque3.begin();
		iterPos != deque3.end();
		++iterPos )
	{
		cout << "deque 3 : " << *iterPos << endl;
	}

	for( deque< int >::iterator iterPos = deque1.begin();
		iterPos != deque1.end();
		++iterPos )
	{
		cout << "deque 1 : " << *iterPos << endl;
	}
}
결과



deque에서 자주 사용하는 deque의 멤버를 중심으로 설명하였습니다. 여기에서 설명하지 않은 나머지 deque 멤버의 사용법은 여기(http://msdn.microsoft.com/en-us/library/8tk0b6f0.aspx)에서 참고해 주세요. 앞에서 여러 번 언급했듯이 deque 사용법이 이전 회의 vector와 거의 같습니다. 위 예제에서 deque를 vector로 바꾸어도 push_front를 제외하고는 문제없이 컴파일할 수 있습니다. deque와 vector는 사용법은 비슷하나 자료구조는 완전히 다릅니다. 앞과 끝에 데이터를 추가, 삭제하는 일이 대부분이라면 deque가 vector보다 좋지만, 그렇지 않다면 vector를 사용하는 쪽이 훨씬 더 좋습니다. STL 컨테이너는 사용이 어렵다기보다는 언제 어떤 STL 컨테이너를 사용해야 알맞은지 선택하기가 어렵습니다. STL 컨테이너를 올바른 장소에 사용하려면 컨테이너의 자료구조가 무엇인지 확실하게 알고 있어야 합니다. 만약 자료구조를 잘 모른다면 꼭 자료구조 공부를 STL 공부보다 먼저 해야 합니다.

5.6. 과제

1. deque를 사용하여 FIFO, LIFO 방식의 스택을 만들어 보세요.

2. deque를 사용하여 Undo, Redo 구현해 보세요.
간단한 예제로 deque의 원리만 익혀보는 과제입니다. Deque에 숫자를 저장하고 Undo를 하면 가장 마지막에 넣은 데이터를 빼고 Redo를 하면 뺀 데이터를 다시 넣습니다. deque를 2개 사용하면 횟수 제한이 없는 Undo, Redo를 구현할 수 있습니다.


[출처 : http://www.hanb.co.kr/network/view.html?bi_id=1616]
제공 : 한빛 네트워크
저자 : 최흥배
이전기사 : 이번 회는 STL 컨테이너 라이브러리 중 하나인 deque를 설명합니다. 앞 회의 list, vector 글을 보신 분들은 아시겠지만 STL 컨테이너 라이브러리는 사용하는 방법이 서로 비슷하므로 하나만 잘 알면 다른 컨테이너도 쉽게 배울 수 있습니다. 이전에 연재했던 list나 vector에 대한 글을 보지 않은 분은 꼭 보신 후 이 글을 보기를 권합니다. 또 이미 list나 vector를 알고 있는 분들은 deque의 자료구조 및 특징을 잘 파악하기를 바랍니다.

5.1 deque의 자료구조

deque의 자료구조는 이름과 같이 Deque(Double Ended Queue) 자료구조입니다. Deque 자료구조는 Queue 자료구조와 비슷하므로 먼저 Queue 자료구조를 설명하겠습니다. Queue는 선형 리스트로 선입선출(FIFO) 방식을 사용합니다. 아래 그림처럼 시작과 끝을 가리키는 포인터로 삽입과 삭제를 합니다.

null
[그림 1] 큐(Queue) 자료구조

[그림 1]에서 F(Front)는 가장 앞에 있는 것을 가리키며 삭제 작업을 할 때 사용합니다. 위 그림에서 삭제를 한다며 ‘a’는 없어지고 F는 ‘b’를 가리킵니다. R(Rear)은 가장 마지막에 있는 것을 가리키며 삽입 작업을 할 때 사용합니다. 위 그림에서 ‘g’를 추가하면 ‘f’ 다음에 위치하며 R은 ‘f’를 가리킵니다.
Queue는 앞으로는 삭제, 뒤로는 삽입을 할 때 사용합니다.
OS의 작업 스케줄링처럼 입력 순서대로 처리를 할 때 Queue를 사용하면 좋습니다. Deque과 Queue와 다른 점은 삽입과 삭제를 한쪽이 아닌 앞, 뒤 양쪽에서 할 수 있다는 것만 다르며 Queue와 같습니다.


[그림 2] 덱(Deque) 자료구조

[그림 1]과 다르게 [그림 2]를 보면 앞과 뒤에서 삽입과 삭제를 할 수 있습니다. Deque는 Stack과 Queue의 장점을 모은 것으로 FIFO 방식과 LIFO 방식 둘 다 사용할 수 있습니다.

5.2 Deque의 특징

Deque의 장점을 정리하면 아래와 같습니다.

1. 크기가 가변적이다.
리스트와 같이 데이터를 담을 수 있는 크기가 가변적입니다.

2. 앞과 뒤에서 삽입과 삭제가 좋다.
Deque가 다른 자료구조와 가장 다른 점으로 앞과 뒤에서 삽입, 삭제가 좋습니다.

3. 중간에 데이터 삽입, 삭제가 용이하지 않다.
데이터를 중간에 삽입하거나 삭제하는 것은 피해야 합니다. 삽입과 삭제를 중간에 한다면 삽입하거나 삭제한 위치 앞뒤 데이터를 모두 이동해야 합니다.


[그림 3] 데이터 g를 중간에 삽입하는 과정


[그림 4] 데이터 c를 삭제하는 과정

4. 구현이 쉽지 않다.
Deque은 Stack과 Queue가 결합된 자료구조로 연결 리스트보다 구현하기가 더 어렵습니다.

5. 랜덤 접근이 가능하다.

연결 리스트처럼 리스트를 탐색하지 않고 원하는 요소에 바로 접근할 수 있습니다.

5.3 deque를 사용하는 경우

Deque의 특징을 고려할 때 다음과 같은 경우에 사용하면 좋습니다.

1. 앞과 뒤에서 삽입, 삭제를 한다.
이것이 deque를 사용하는 가장 큰 이유입니다. 대부분 작업이 데이터를 앞이나 뒤에 삽입, 삭제를 한다면 STL 컨테이너 라이브러리 중에서 deque를 사용할 때 성능이 가장 좋습니다.

2. 저장할 데이터 개수가 가변적이다.
저장할 데이터 개수를 미리 알 수 없어도 deque는 크기가 동적으로 변하므로 유연하게 사용할 수 있습니다.

3. 검색을 거의 하지 않는다.
많은 데이터를 저장한다면 앞 회에서 여러 번 언급했듯이 map, set, hash_map 중 하나를 선택해서 사용하는 편이 좋습니다.

4. 데이터 접근을 랜덤하게 하고 싶다.
vector와 같이 랜덤 접근이 가능합니다. 사용하는 방법도 같습니다.

5.4 deque VS vector

deque는 전체적으로 멤버 함수의 기능이나 사용 방법이 vector와 거의 같습니다. vector는 삽입과 삭제를 뒤(back)에서만 해야 성능이 좋지만, deque는 삽입과 삭제를 앞과 뒤에서 해도 좋으며 앞뒤 삽입, 삭제 성능도 vector보다 좋습니다. 하지만, deque는 앞뒤에서 삽입, 삭제하는 것을 제외한 다른 위치에서의 삽입과 삭제는 vector보다 성능이 좋지 않습니다.
  deque vector
크기 변경 가능 O O
앞에 삽입, 삭제 용이 O X
뒤에 삽입, 삭제 용이 O O
중간 삽입, 삭제 용이 X X
순차 접근 가능 O O
랜덤 접근 가능 O X
[표 1] deque와 vector의 차이

deque와 vector를 비교할 때 고려해야 되는 점은
deque는 앞과 뒤에서 삽입과 삭제 성능이 vector보다 더 좋다.
deque는 앞과 뒤 삽입, 삭제를 제외한 기능은 vector보다 성능이 좋지 못하다.


게임 서버에서 deque를 사용하는 경우

게임 서버는 클라이언트에서 보낸 패킷을 차례대로 처리합니다. 서버에서 네트워크 데이터를 받는 함수에서 데이터를 받으면 패킷으로 만든 후 받은 순서대로 순차적으로 처리합니다. 이렇게 순차적으로 저장한 패킷을 처리할 때는 deque가 가장 적합한 자료구조입니다. 다만, 실제 현업에서는 이 부분에 STL의 deque를 사용하지 않는 경우가 종종 있습니다. 이유는 네트워크에서 데이터를 받아 패킷으로 만들어 저장하고, 그 패킷을 처리하는 부분은 게임 서버의 성능 면에서 가장 중요한 부분이므로 deque보다 더 빠르게 처리하기를 원하므로 독자적인 자료구조를 만들어 사용합니다(즉, 범용성보다는 성능을 우선시합니다).

5.5 deque 사용 방법

deque를 사용하려면 deque 헤더 파일을 포함합니다.
#include 
deque 형식은 아래와 같습니다.
deque< 자료 type > 변수 이름
int를 사용하는 deque 선언은 아래와 같습니다.
deque< int > deque1;
deque도 동적 할당을 할 수 있습니다.
deque< 자료 type >* 변수 이름 = new deque< 자료 type >;
deque< int >* deque1 = new deque< int >;

5.5.1 deque의 주요 멤버들

deque 멤버 중 일반적으로 자주 사용하는 멤버들입니다. vector에 없는 pop_front와 push_front가 있다는 것 빼고는 vector의 기능과 같습니다.

멤버 설명
assign 특정 원소로 채운다
at 특정 위치의 원소의 참조를 반환
back 마지막 원소의 참조를 반환
begin 첫 번째 원소의 랜던 접근 반복자를 반환
clear 모든 원소를 삭제
empty 아무것도 없으면 true 반환
End 마지막 원소 다음의(미 사용 영역) 반복자를 반환
erase 특정 위치의 원소나 지정 범위의 원소를 삭제
front 첫 번째 원소의 참조를 반환
insert 특정 위치에 원소 삽입
pop_back 마지막 원소를 삭제
pop_front 첫 번째 원소를 삭제
push_back 마지막에 원소를 추가
push_front 제일 앞에 원소 추가
rbegin 역방향으로 첫 번째 원소의 반복자를 반환
rend 역방향으로 마지막 원소 다음의 반복자를 반환
reserve 지정된 크기의 저장 공간을 확보
size 원소의 개소를 반환
swap 두 개의 vector의 원소를 서로 맞바꾼다
[표 2] 자주 사용하는 deque 멤버

5.5.2. 기본 사용 멤버

deque의 가장 기본적인(추가, 삭제, 접근 등) 사용법을 설명합니다.
멤버 원형 설명
at reference at( size_type _Pos );
const_reference at( size_type _Pos ) const;
특정 위치의 원소의 참조를 반환
back reference back( );
const_reference back( ) const;
마지막 원소의 참조를 반환
begin const_iterator begin() const;
iterator begin();
첫 번째 원소의 랜던 접근 반복자를 반환
clear void clear(); 모든 원소를 삭제
empty bool empty() const; 아무것도 없으면 true 반환
end iterator end( );
const_iterator end( ) const;
마지막 원소 다음의(미 사용 영역) 반복자를 반환
front reference front( );
const_reference front( ) const;
첫 번째 원소의 참조를 반환
pop_back void pop_back(); 마지막 원소를 삭제
pop_front void pop_front( ); 첫 번째 원소를 삭제
push_back void push_back( const Type& _Val ); 마지막에 원소를 추가
push_front void push_front( const Type& _Val ); 제일 앞에 원소 추가
rbegin reverse_iterator rbegin( );
const_reverse_iterator rbegin( ) const;
역방향으로 첫 번째 원소의 반복자를 반환
rend const_reverse_iterator rend( ) const;
reverse_iterator rend( );
역방향으로 마지막 원소 다음의 반복자를 반환
size size_type size() const; 원소의 개수를 반환
[표 3] 추가, 삭제, 접근 등에 사용하는 멤버들


[그림 5] deque의 추가, 삭제, 접근 멤버들

추가

앞과 뒤에 추가를 할 수 있습니다. 보통 deque를 사용할 때는 뒤에 추가를 하고 앞에서는 삭제를 합니다. 앞쪽 추가는 push_front()를 사용합니다.
deque< int > deque1;
deque1.push_front( 100 );
뒤에 추가할 때는 push_back()을 사용합니다.
deque< int > deque1;
deque1.push_back( 200 );
삭제

앞과 뒤에서 삭제 할 수 있습니다. 앞에서 삭제를 할 때는 pop_front()를 사용합니다.
deque1.pop_front();
뒤에서 삭제를 할 때는 pop_back()를 사용합니다.
deque1.pop_back();
접근

첫 번째 위치의 반복자를 얻을 때는 begin()을 사용합니다.
deque< int >::iterator IterBegin = deque1.begin();
cout << *IterBegin << endl;
반복자로 다른 원소에 접근을 할 때는 반복자에 ‘++’ 이나 ‘–-‘을 사용합니다.
deque< int >::iterator IterPos = deque1.begin();

// 두 번째 위치로 이동
++IterPos;
// 첫 번째 위치로 이동
--IterPos;
end()는 deque에 저장된 원소 중 마지막 다음 위치, 즉 사용하지 못하는 영역을 가리킵니다. 보통 반복문에서 컨테이너에 남은 원소가 있는지 조사할 때 주로 사용합니다.
deque< int >::iterator IterEnd = deque1.end();
for(deque< int >::iterator IterPos = deque1.begin; IterPos != IterEnd;
     ++IterPos )
{
   ……
}
첫 번째 위치에 있는 데이터를 얻을 때는 front(), 마지막 위치에 있는 데이터를 얻을 때는 back()을 사용합니다.
int& FirstValue = deque1.front();
int& LastValue = deque1.back();
begin()과 end()는 순방향으로 앞과 뒤를 가리키고, 역방향으로는 rbegin()과 rend()를 사용합니다. 특정 위치에 있는 데이터를 얻을 때는 at()이나 배열 식 접근([])을 사용합니다.
int& Value2 = deque1.at(1); // 두 번째 위치
int Value3 = deque1[2];      // 세 번째 위치
모두 삭제

clear()를 사용하면 저장한 모든 데이터를 삭제합니다.
deque1.clear();
데이터 저장 여부

deque에 저장한 데이터가 있는지 없는지는 empty()로 조사합니다. 데이터가 있으면 false, 없다면 true를 반환합니다.
bool bEmpty = deque1.empty();
저장된 원소 개수 조사

size()로 deque에 저장된 데이터 개수를 조사합니다.
deque< int >::size_type TotalCount = deque1.size();
지금까지 설명한 deque 멤버들의 사용법을 보면 전 회에 설명한 vector와 같다는 것을 충분히 알 수 있을 것입니다. vector를 공부하신 분들은 아주 쉽죠? ^^ 이후에 소개하는 내용도 vector에서 설명한 것과 같으니 편안하게 잘 따라와 주세요.


[출처 : http://www.hanb.co.kr/network/view.html?bi_id=1615]
제공 : 한빛 네트워크
저자 : 최흥배
이전기사 : 이번 회는 이전 회에 설명한 list와 같은 STL의 컨테이너 라이브러리인 vector에 대해서 이야기합니다. vector는 STL에서 가장 자주 사용합니다. 프로그래밍을 할 때 가장 자주 사용하는 자료구조는 배열입니다. vector는 배열을 대체하여 사용할 수 있습니다. vector는 배열과 비슷한 면이 많아서 STL 컨테이너 중에서 이해하기가 가장 쉽고 또 어디에 사용해야 하는지 알기 쉽습니다. 앞서 연재한 list에 대한 글을 보신 분들은(또는 아시는 분들은) vector와 사용 방법이 비슷한 점이 많아서 list보다 훨씬 더 빠르게 이해하리라 생각합니다. list에서 이미 언급한 몇몇 부분은 다시 언급하지 않으니 list에 대한 글을 보지 않으신 분은 꼭 보시기 바랍니다.

4.1 vector의 자료구조

처음 말했듯이 vector의 자료구조는 배열과 비슷합니다. Wikipedia에서 배열은 “번호와 번호에 대응하는 데이터로 이루어진 자료구조를 나타냅니다. 일반적으로 배열에는 같은 종류의 데이터가 순차적으로 저장된다’고 설명합니다. 문자 A, B, C, D, E를 배열에 저장한다면 아래 그림과 같이 저장합니다.


[그림 1] A, B, C, D, E가 저장된 배열

배열의 크기는 고정이지만 vector는 동적으로 변하는 점이 vector와 배열 자료구조의 큰 차이점입니다.

4.2 배열의 특징

1. 배열의 크기는 고정이다.
배열은 처음에 크기를 설정하면 이후에 크기를 변경하지 못합니다. 처음 설정한 크기를 넘어서 데이터를 저장할 수 없습니다. [그림 1]의 배열은 A, B, C, D, E만 저장할 수 있게 5개의 크기로 만들어져 있습니다. 배열은 크기가 고정이므로 더 이상 새로운 것을 넣을 수 없습니다.

2. 중간에 데이터 삽입, 삭제가 용이하지 않다.
배열은 데이터를 순차적으로 저장합니다. 중간에 데이터를 삽입하면 삽입한 위치 이후의 데이터는 모두 뒤로 하나씩 이동해야 합니다. 또 중간에 있는 데이터를 삭제하면 삭제한 위치 이후의 데이터는 모두 앞으로 하나씩 이동해야 합니다.


[ 그림 2] 중간에 삽입. F를 C와 D 사이에 삽입. D와 E를 뒤로 이동 시킨 후 빈 공간에 넣는다


[그림 3] 중간에 삭제. C를 삭제. 삭제 후 D와 E가 앞으로 이동

3. 구현이 쉽다.
배열은 크기가 고정이며 중간 삭제 및 삽입에 대한 특별한 기능이 없는 아주 단순한 자료구조입니다. 제일 처음 프로그래밍을 배울 때 배우는 자료구조가 배열 일 정도로 구현이 쉽습니다.

4. 랜덤 접근이 가능하다.
배열은 데이터를 순차적으로 저장하므로 랜덤 접근이 가능합니다.

4.3 vector를 사용해야 하는 경우

1. 저장할 데이터 개수가 가변적이다.
배열과 vector의 가장 큰 차이점은 ‘배열은 크기가 고정이고 vector는 크기가 동적으로 변한다’입니다. 저장할 데이터 개수를 미리 알 수 없다면 vector를 사용 하는 편이 좋습니다.

2. 중간에 데이터 삽입이나 삭제가 없다.
vector는 배열처럼 데이터를 순차적으로 저장합니다. 중간에 데이터 삭제 및 삽입을 하면 배열과 같은 문제가 발생합니다. vector는 가장 뒤에서부터 데이터를 삭제 하거나 삽입하는 경우에 적합합니다.

3. 저장할 데이터 개수가 적거나 많은 경우 빈번하게 검색하지 않는다.
데이터를 순차적으로 저장하므로 많은 데이터를 저장한다면 검색 속도가 빠르지 않습니다. 검색을 자주한다면 map이나 set, hash_map을 사용해야 합니다.

4. 데이터 접근을 랜덤하게 하고 싶다.
vector는 배열 같은 특성이 있어서 랜덤 접근이 가능합니다. 특정 데이터가 저장된 위치를 안다면 랜덤 접근을 사용하는 쪽이 성능이 좋고, 사용하기도 간편합 니다. 예를 들면 온라인 게임 제작 시 아이템 번호를 순차적으로 부여한다고 가정합니다. 아이템 데이터를 vector에 저장하면 아이템 개수가 늘어나더라도 코 드를 수정하지 않아도 되며, 아이템 코드 7번은 언제나 7번째 위치에 있으므로 랜덤 접근으로 빠르고 쉽게 접근할 수 있습니다. 위에 열거한 배열의 특징과 vector의 특징을 잘 숙지하여 기존에 배열을 사용한 부분에 vector를 사용하면 배열의 단점을 없앤 유지보수성이 좋은 코드를 만들게 됩니다.

4.4 vector vs. list

vector 사용법을 보면 list와 비슷한 부분도 있고 다른 부분도 있음을 알게 되리라 생각합니다. vector과 list의 차이점을 잘 이해한 후 올바르게 사용해야 됩니다 . vector와 list의 차이를 정리하면 아래 표와 같습니다.

vector List
크기 변경 가능 O O
중간 삽입, 삭제 용이 X O
순차 접근 가능 O O
랜덤 접근 가능 O X
[표 1] vector와 list의 차이

[표 1]을 보시면 아시겠지만 vector와 list의 차이점은 크게 2가지입니다.
  • 중간 삽입, 삭제
  • 랜덤 접근
중간 삽입, 삭제가 없고 랜덤 접근을 자주 해야 된다면 vector가 좋고, 중간 삽입, 삭제가 자주 있으며 랜덤 접근이 필요 없으면 list가 좋습니다.

[참고]
중간 삽입 삭제가 있다면 무조건 list를 사용해야 할까요? list와 vector의 차이점을 보면 중간 삽입, 삭제가 자주 일어나는 자료구조는 list 사용이 정답입니다. 그렇다고 list 사용이 항상 정답은 아닙니다. 만약 저장하는 데이터의 개수가 적고 랜덤 접근을 하고 싶은 경우에는 vector를 사용해도 좋습니다.

제가 하는 일을 예를 들면 대부분의 온라인 캐주얼 게임은 방을 만들어서 방에 들어온 유저끼리 게임을 합니다. 방은 유저가 들어 오기도 하고 나가기도 합니 다. 중간 삽입, 삭제가 자주 일어나지만 방의 유저 수는 대부분 적습니다. 이런 경우 중간 삽입, 삭제로 데이터를 이동해도 전체적인 성능 측면에서는 문제가 되지 않습니다. 방에 있는 유저를 저장한 위치를 알고 있다면 유저 데이터에 접근할 때 list는 반복문으로 해당 위치까지 순차적으로 접근해야 하지만 vector는 바로 랜덤 접근이 가능하고 성능면에서도 더 좋습니다. 또한, 방에 있는 모든 유저 데이터에 접근할 때 list는 반복자(Iterator)로 접근하지만, vector는 일반 배열 처럼 접근하므로 훨씬 간단하게 유저 데이터에 접근할 수 있습니다.

저장하는 데이터 개수가 적은 경우는 대부분 list 보다는 vector를 사용하는 편이 더 좋습니다. vector와 list의 차이점만으로 둘 중 어느 것을 사용할지 선택하기 보다는 전체적인 장, 단점을 파악하고나서 선택하는 것이 좋습니다.

4.5 vector 사용 방법

vector를 사용하려면 vector 헤더 파일을 포함해야 합니다.
#include <vector>
vector 형식은 아래와 같습니다.
vector< 자료 type > 변수 이름
vector를 int 형에 대해 선언했습니다.
vector< int > vector1;
선언 후 vector를 사용합니다. vector도 list처럼 동적 할당이 가능합니다.
vector < 자료 type >* 변수 이름 = new vector< 자료 type >;
vector< int >* vector1 = new vector< int>;

4.5.1 vector의 주요 멤버들

vector 멤버 중 일반적으로 자주 사용하는 멤버들은 아래와 같습니다.

멤버 설명
assign 특정 원소로 채운다
at 특정 위치의 원소의 참조를 반환
back 마지막 원소의 참조를 반환
begin 첫 번째 원소의 랜던 접근 반복자를 반환
clear 모든 원소를 삭제
empty 아무것도 없으면 true 반환
End 마지막 원소 다음의(미 사용 영역) 반복자를 반환
erase 특정 위치의 원소나 지정 범위의 원소를 삭제
front 첫 번째 원소의 참조를 반환
insert 특정 위치에 원소 삽입
pop_back 마지막 원소를 삭제
push_back 마지막에 원소를 추가
rbegin 역방향으로 첫 번째 원소의 반복자를 반환
rend 역방향으로 마지막 원소 다음의 반복자를 반환
reserve 지정된 크기의 저장 공간을 확보
size 원소의 개소를 반환
swap 두 개의 vector의 원소를 서로 맞바꾼다
[표 2] 자주 사용하는 vector 멤버

4.5.1.1 기본 사용 멤버

vector의 가장 기본적인(추가, 삭제, 접근 등) 사용법을 설명 하겠습니다. [표 3]과 [그림 4]를 참고 해주세요

멤버 원형 설명
at reference at( size_type _Pos );
const_reference at( size_type _Pos ) const;
특정 위치의 원소의 참조를 반환
back reference back( );
const_reference back( ) const;
마지막 원소의 참조를 반환
begin const_iterator begin() const;
iterator begin();
첫 번째 원소의 랜덤 접근 반복자를 반환
clear void clear(); 모든 원소를 삭제
empty bool empty() const; 아무것도 없으면 true 반환
end iterator end( );
const_iterator end( ) const;
마지막 원소 다음의(미 사용 영역) 반복자를 반환
front reference front( );
const_reference front( ) const;
첫 번째 원소의 참조를 반환
pop_back void pop_back(); 마지막 원소를 삭제
push_back void push_back( const Type& _Val ); 마지막에 원소를 추가
rbegin reverse_iterator rbegin( );
const_reverse_iterator rbegin( ) const;
역방향으로 첫 번째 원소의 반복자를 반환
rend const_reverse_iterator rend( ) const;
reverse_iterator rend( );
역방향으로 마지막 원소 다음의 반복자를 반환
size size_type size() const; 원소의 개수를 반환
[표 3] 추가, 삭제, 접근 등에 사용하는 멤버들


[그림 4] vector의 추가, 삭제, 접근 멤버를 나타내고 있다

추가
기본적으로 원소의 마지막 위치에 추가하며 push_back을 사용합니다. 처음이나 중간 위치에 추가할 때는 다음에 소개할 insert를 사용합니다.
vector< int > vector1;
vector1.push_back( 1 );
삭제
기본적으로 마지막 위치의 원소를 삭제하며 pop_back을 사용합니다. 처음이나 중간에 있는 원소를 삭제할 때는 다음에 소개할 erase를 사용합니다.
vector1.pop_back();
접근
첫 번째 위치의 반복자를 반환할 때는 begin()을 사용합니다. 첫 번째 원소의 참조를 반환할 때는 front()를 사용합니다.
vector< int >::iterator IterBegin = vector1.begin();
cout << *IterBegin << endl;

int& FirstValue = vector1.front();
const int& refFirstValue = vector1.front();
마지막 다음의 영역(미 사용 영역)을 가리키는 반복자를 반환할 때는 end()를 사용합니다. 마지막 원소의 참조를 반환할 때는 back()을 사용합니다.
;
vector< int >::iterator IterEnd = vector1.end();
for(vector< int >::iterator IterPos = vector1.begin; IterPos != vector1.end();
     ++IterPos )
{
   ……..
}

int& LastValue = vector1.back();
const int& refLastValue = vector1.back();
rbegin()과 rend()는 방향이 역방향이라는 점만 다를 뿐, 나머지는 begin()과 end()와 같습니다. 특정 위치에 있는 원소를 접근할 때는 at()을 사용하면 됩니다.
int& Value1 = vector1.at(0); // 첫 번째 위치
const int Value2 = vector1.at(1); // 두 번째 위치
배열 식 접근도 가능합니다. vector를 사용할 때 보통 이 방식으로 자주 사용합니다.
int Value = vector1[0]; // 첫 번째 위치
모두 삭제
저장한 모든 데이터를 삭제할 때는 clear()를 사용합니다.
vector1.clear();
데이터 저장 여부
vector에 저장한 데이터가 있는지 없는지는 empty()로 조사합니다. empty()는 데이터가 있으면 false, 없다면 true를 반환합니다.
bool bEmpty = vector1.empty();
vector에 저장된 원소 개수 알기
size()를 사용하여 vector에 저장 되어 있는 원소 개수를 조사합니다.
vector< int >::size_type Count = vector1.size();
위에 설명한 멤버들을 사용하는 아래의 예제 코드를 보면서 vector의 기본 사용 방법을 정확하게 숙지해 주세요

[리스트 1] 온라인 게임의 게임 방의 유저 관리
#include <vector>
#include <iostream>

using namespace std;


// 방의 유저 정보
struct RoomUser
{
	int CharCd; // 캐릭터 코드
	int Level;  // 레벨
};


void main()
{
	// 유저1
	RoomUser RoomUser1;
	RoomUser1.CharCd = 1;	RoomUser1.Level = 10;

	// 유저2
	RoomUser RoomUser2;
	RoomUser2.CharCd = 2;	RoomUser2.Level = 5;
	// 유저3
	RoomUser RoomUser3;
	RoomUser3.CharCd = 3;	RoomUser3.Level = 12;

	// 방의 유저들을 저장할 vector
	vector< RoomUser > RoomUsers;


	// 추가
	RoomUsers.push_back( RoomUser1 );
	RoomUsers.push_back( RoomUser2 );
	RoomUsers.push_back( RoomUser3 );

	// 방에 있는 유저 수
	int UserCount = RoomUsers.size();

	// 방에 있는 유저 정보 출력
	// 반복자로 접근 -  순방향
	for( vector< RoomUser >::iterator IterPos = RoomUsers.begin();
		IterPos != RoomUsers.end();
		++IterPos )
	{
		cout << "유저코드 : " << IterPos->CharCd << endl;
		cout << "유저레벨 : " << IterPos->Level << endl;
	}
	cout << endl;
	
	// 반복자로 접근- 역방향
	for( vector< RoomUser >::reverse_iterator IterPos = RoomUsers.rbegin();
		IterPos != RoomUsers.rend();
		++IterPos )
	{
		cout << "유저코드: " << IterPos->CharCd << endl;
		cout << "유저레벨: " << IterPos->Level << endl;
	}
	cout << endl;

	// 배열 방식으로 접근
	for( int i = 0; i < UserCount; ++i )
	{
		cout << "유저 코드 : " << RoomUsers[i].CharCd << endl;
		cout << "유저 레벨 : " << RoomUsers[i].Level << endl;
	}
	cout << endl;

	// 첫 번째 유저 데이터 접근
	RoomUser& FirstRoomUser = RoomUsers.front();
	cout << "첫 번째 유저의 레벨 : " << FirstRoomUser.Level << endl << endl;

	RoomUser& LastRoomUser = RoomUsers.back();
	cout << "마지막 번째 유저의 레벨: " << LastRoomUser.Level << endl << endl;

	// at을 사용하여 두 번째 유저의 레벨을 출력
	RoomUser& RoomUserAt = RoomUsers.at(1);
	cout << "두 번째 유저의 레벨: " << RoomUserAt.Level << endl << endl;

	// 삭제
	RoomUsers.pop_back();

	UserCount = RoomUsers.size();
	cout << "현재 방에 있는 유저 수: " << UserCount << endl << endl;


	// 아직 방에 유저가 있다면 모두 삭제한다.
	if( false == RoomUsers.empty() )
	{
		RoomUsers.clear();
	}

	UserCount = RoomUsers.size();
	cout << "현재 방에 있는 유저 수: " << UserCount << endl;
}
결과



혹시 랜덤 접근이 무엇인지 잘 이해하지 못한 분은 [리스트 1]의 예제 코드에서 배열 방식으로 접근하는 부분을 잘 보세요. 배열처럼 접근 하는 것을 랜덤 접근 이 가능하다고 말합니다. 랜덤 접근이 안 되는 list에서는 오직 반복자로 순차 접근만 가능합니다.

4.5.1.2 insert

insert는 지정된 위치에 삽입하며, 세 가지 방식이 있습니다. list의 insert와 사용 방법이 같습니다. 세 가지 원형은 각각 지정한 위치에 삽입, 지정한 위치에 지 정한 개수만큼 삽입, 지정한 위치에 지정 범위에 있는 것을 삽입합니다. vector의 insert를 사용할 때에는 삽입한 위치 이후의 원소들이 모두 뒤로 이동함을 꼭 숙지하셔야 됩니다.
원형 : iterator insert( iterator _Where, const Type& _Val );
         void insert( iterator _Where, size_type _Count, const Type& _Val );
         template<class InputIterator> void insert( iterator _Where, InputIterator _First, 
                                                                         InputIterator _Last );

[그림 5] vector의 insert

첫 번째 insert는 지정한 위치에 데이터를 삽입합니다.
vector< int >::iterator iterInsertPos = vector1.begin();
vector1.insert( iterInsertPos, 100 );
이 코드는 100을 첫 번째 위치에 삽입합니다. 두 번째 insert는 지정한 위치에 데이터를 횟수만큼 삽입합니다.
iterInsertPos = vector1.begin();
++iterInsertPos;
vector1.insert( iterInsertPos, 2, 200 );
두 번째 위치에 200을 두 번 추가합니다. 세 번째 insert는 지정한 위치에 복사 할 vector의 (복사하기를 원하는 영역의)시작과 끝 반복자가 가리키는 영역의 모 든 요소를 삽입합니다.
vector< int > vector2;
list2.push_back( 500 );
list2.push_back( 600 );
list2.push_back( 700 );

iterInsertPos = vector1.begin();
vector1.insert( ++iterInsertPos, vector2.begin(), vector2.end() );
vector1의 두 번째 위치에 vector2의 모든 요소를 삽입합니다. 위에서 설명한 insert의 세 가지 방법을 사용한 전체 코드입니다. 참고로 이 예제는 이전 회의 list 의 insert에서 소개했던 코드와 같습니다. 오직 list 대신 vector를 사용했다는 것만 다릅니다.

[리스트 2] insert 사용
void main()
{
	vector< int > vector1;

	vector1.push_back(20);
	vector1.push_back(30);


	cout << "삽입 테스트 1" << endl;

	// 첫 번째 위치에 삽입한다.
	vector< int >::iterator iterInsertPos = vector1.begin();
	vector1.insert( iterInsertPos, 100 );
	
	// 100, 20, 30 순으로 출력한다.
	vector< int >::iterator iterEnd = vector1.end();
	for(vector< int >::iterator iterPos = vector1.begin(); 
		iterPos != iterEnd; 
		++iterPos )
	{
		cout << "vector1 : " << *iterPos << endl;
	}


	cout << endl << "삽입 테스트 2" << endl;

	// 두 번째 위치에 200을 2개 삽입한다.
	iterInsertPos = vector1.begin();
	++iterInsertPos;
	vector1.insert( iterInsertPos, 2, 200 );
	
	// 100, 200, 200, 20, 30 순으로 출력한다.
	iterEnd = vector1.end();
	for(vector< int >::iterator iterPos = vector1.begin(); 
		iterPos != iterEnd; 
		++iterPos )
	{
		cout << "vector1 : " << *iterPos << endl;
	}


	cout << endl << "삽입 테스트 3" << endl;

	vector< int > vector2;
	vector2.push_back( 1000 );
	vector2.push_back( 2000 );
	vector2.push_back( 3000 );

	// 두 번째 위치에 vecter2의 모든 데이터를 삽입한다.
	iterInsertPos = vector1.begin();
	vector1.insert( ++iterInsertPos, vector2.begin(), vector2.end() );
	
	// 100, 1000, 2000, 3000, 200, 200, 20, 30 순으로 출력한다.
	iterEnd = vector1.end();
	for(vector< int >::iterator iterPos = vector1.begin(); 
		iterPos != iterEnd; 
		++iterPos )
	{
		cout << "vector1 : " << *iterPos << endl;
	}
}
결과



4.5.1.3 erase

반복자로 특정 위치의 요소를 삭제할 때는 erase를 사용합니다. 사용 방식은 두 가지가 있습니다. 하나는 지정한 위치의 요소를 삭제하고, 다른 하나는 지정한 범위의 요소를 삭제합니다. 마지막 위치 이외의 곳에서 erase를 할 때는 삭제한 위치 이후의 모든 원소들이 앞으로 이동한다는 것을 꼭 숙지하셔야 됩니다.
원형 : iterator erase( iterator _Where );
         iterator erase( iterator _First, iterator _Last );

[그림 6] vector의 erase

첫 번째 erase는 지정한 위치의 요소를 삭제합니다. 다음은 첫 번째 요소를 삭제하는 코드입니다.
vector1.erase( vector1.begin() );
두 번째 erase는 지정한 반복자 요소만큼 삭제합니다. 다음 코드는 vector1의 첫 번째 요소에서 마지막까지 모두 삭제합니다.
vector1.erase( vector1.begin(), vector1.end() );
다음은 erase 사용을 보여주는 예제입니다.

[리스트 3] erase 사용
void main()
{
	vector< int > vector1;

	vector1.push_back(10);
	vector1.push_back(20);
	vector1.push_back(30);
	vector1.push_back(40);
	vector1.push_back(50);

	int Count = vector1.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 1 : " << vector1[i] << endl;
	}
	cout << endl;


	cout << "erase 테스트 1" << endl;

	// 첫 번째 데이터 삭제
	vector1.erase( vector1.begin() );
	
	// 20, 30, 40, 50 출력
	Count = vector1.size();		
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 1 : " << vector1[i] << endl;
	}
	

	cout << endl << "erase 테스트" << endl;

	// 첫 번째 데이터에서 마지막까지 삭제한다.
	vector< int >::iterator iterPos = vector1.begin();
	vector1.erase( iterPos, vector1.end() );
	
	if( vector1.empty() )
	{
		cout << "vector1에 아무 것도 없습니다" << endl;
	}
}
결과



4.5.1.4 assign

vector를 어떤 특정 데이터로 채울 때는 assign을 사용하면 됩니다. 사용 방식은 두 가지가 있습니다. 첫 번째는 특정 값으로 채우는 방법이고, 두 번째는 다른 vector의 반복자로 지정한 영역에 있는 원소로 채우는 방법입니다. 만약 assign을 사용한 vector에 이미 데이터가 있다면 기존의 것은 모두 지우고 채웁니다.
원형 : void assign( size_type _Count, const Type& _Val );
         template<class InputIterator> void assign( InputIterator _First, InputIterator _Last );

[그림 7] assign

첫 번째 assign은 지정 데이터를 지정 개수만큼 채워줍니다. 숫자 4를 7개 채웁니다.
vector1.assign( 7, 4 );
두 번째 assign은 다른 vector의 반복자로 지정한 영역으로 채워줍니다.
vector1.erase( vector1.begin(), vector1.end() );
다음은 assign을 사용법을 보여주는 예제입니다.

[리스트 4] assign
void main()
{
	vector< int > vector1;
	
	// 4를 7개 채운다.
	vector1.assign( 7, 4 );
	
	int Count = vector1.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 1 : " << vector1[i] << endl;
	}
	cout << endl;



	vector< int > vector2;
	vector2.push_back(10);
	vector2.push_back(20);
	vector2.push_back(30);
	

	// vector2의 요소로 채운다
	vector1.assign( vector2.begin(), vector2.end() );
	Count = vector1.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 1 : " << vector1[i] << endl;
	}
	cout << endl;
}
결과



4.5.1.5 reserve

vector는 사용할 메모리 영역을 처음 선언할 때 정해진 값만큼 할당한 후 이 크기를 넘어서게 사용하면 현재 할당한 크기의 2배의 크기로 재할당합니다. vector에 어느 정도의 데이터를 저장할지 가늠할 수 있고, vector 사용 도중에 재할당이 일어나는 것을 피하려면 사용할 만큼의 크기를 미리 지정해야 합니다. 참고로 reserve로 지정할 수 있는 크기는 vector에서 할당하는 최소의 크기보다는 커야 합니다.
원형 : void reserve( size_type _Count );

[그림 8] reserve

10개의 원소를 채울 수 있는 공간 확보
vector1.reserve( 10 );
4.5.1.6 swap

vector1과 vector2가 있을 때 두 개의 vector간에 서로 데이터를 맞바꾸기를 할 때 사용합니다.
원형 : void swap( vector<Type, Allocator>& _Right );
         friend void swap( vector<Type, Allocator >& _Left, vector<Type, Allocator >& _Right );

[그림 9] swap

vector1과 vector2를 swap
vector1.swap( vector2 );
swap을 사용하는 예제입니다.

[리스트 5] Swap
void main()
{
	vector< int > vector1;
	vector1.push_back(1);
	vector1.push_back(2);
	vector1.push_back(3);

	vector< int > vector2;
	vector2.push_back(10);
	vector2.push_back(20);
	vector2.push_back(30);
	vector2.push_back(40);
	vector2.push_back(50);


	int Count = vector1.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 1 : " << vector1[i] << endl;
	}
	cout << endl;


	Count = vector2.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 2 : " << vector2[i] << endl;
	}
	cout << endl;
	cout << endl;

	cout << "vector1과vector2를swap" << endl;

	vector1.swap(vector2);

	Count = vector1.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 1 : " << vector1[i] << endl;
	}
	cout << endl;


	Count = vector2.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 2 : " << vector2[i] << endl;
	}
}
결과



vector 중에서 가장 많이 사용하는 멤버를 중심으로 설명 하였습니다. vector의 모든 멤버를 설명하지는 않았으니 소개하지 않은 나머지 멤버까지 알고 싶다면 마이크로소프트의 MSDN에 나와 있는 것을 참고해 주세요. http://msdn.microsoft.com/en-us/library/sxcsf7y7.aspx

앞 회의 list 글을 보신 분들은 이번에 설명한 vector에서 소개한 front(), push_back(), pop_back(), erase() 등이 list의 멤버들과 사용 방법이나 결과가 같음을 알 수 있습니다. 이것이 STL를 사용하여 얻는 장점 중의 하나입니다.

STL에서 제공하는 컨테이너들은 서로 특성은 다르지만 사용 방법과 결과가 같기 때문에 하나만 잘 알며 다른 것들도 쉽게 배울 수 있습니다. 만약 STL의 컨테 이너를 사용하지 않고 독자적으로 구현하여 사용한다면 각각 사용 방법이 달라서 사용 방법을 배울 때마다 STL보다 더 많은 시간이 필요할 것이며, 함수 이름 을 보고 어떤 동작을 할지 각각의 라이브러리마다 숙지해야 하므로 유지보수에 좋지 않습니다.

vector는 배열과 비슷하고 사용하기 편리하여 많은 곳에서 사용합니다. 그러나 vector의 특성을 제대로 이해하지 못하고 잘못된 곳에 사용하면 심각한 성능 저 하가 일어날 수 있습니다(많은 데이터를 저장하고 있으며 빈번하게 중간에서 삽입, 삭제를 할 때). 그러니 꼭 적합한 장소에 사용해야 합니다.

과제

1. 이전 회의 글 중 ‘3.5 list를 사용한 스택’에서 list를 사용하여 LIFO 방식으로 스택을 만든 예제가 있는데 이것을 vector를 사용하여 만들어 보세요

2. ‘카트 라이더’와 같이 방을 만들어서 게임을 하는 온라인 게임에서 방에 있는 유저를 관리하는 부분을 vector를 사용하여 만들어 보세요. 기본적인 클래스 선언은 제시할 테니 구현만 하면 됩니다.
// 유저 정보
struct UserInfo
{
	char acUserName[21]; // 이름	
	int	Level;       // 레벨  
	int Exp;             // 경험치   
};

// 게임 방의 유저를 관리하는 클래스
// 방에는 최대 6명까지 들어 갈 수 있다.
// 방에 들어 오는 순서 중 가장 먼저 들어 온 사람이 방장이 된다.
class GameRoomUser
{
public:
	GameRoomUser();
	~GameRoomUser();

	// 방에 유저 추가
	bool AddUser( UserInfo& tUserInfo );

	// 방에서 유저 삭제. 
	// 만약 방장이 나가면 acMasterUserName에 새로운 방장의 이름을 설정 해야 된다.
	bool DelUser( char* pcUserName );

	// 방에 유저가 없는 지조사. 없으면 true 반환
	bool IsEmpty();

	// 방에 유저가 꽉 찼는지 조사. 꽉 찼다면 true 반환
	bool IsFull();

	// 특정 유저의 정보
	UserInfo& GetUserOfName( char* pcName );

	// 방장의 유저 정보
	UserInfo& GetMasterUser();

	// 가장 마지막에 방에 들어 온 유저의 정보
	UserInfo& GetUserOfLastOrder();

	// 특정 순서에 들어 온 유저를 쫒아낸다.
	bool BanUser( int OrderNum );

	// 모든 유저를 삭제한다.
	void Clear();
	
private:
	vector< UserInfo > Users;
	char acMasterUserName[21]; // 방장의 이름

};



[출처 : http://www.hanb.co.kr/network/view.html?bi_id=1606]

+ Recent posts