Skip to main content

[C++ Primer Plus] Chapter 16. (5) STL - 알고리즘, 기타 라이브러리

C++ 기초 플러스 책을 읽고 공부한 노트입니다.




알고리즘(algorithm) #

  • 알고리즘
    • 컨테이너와 함께 사용할 수 있는 멤버가 아닌 함수들이다.
    • 알고리즘 함수를 설계하는 주요 두 가지 일반화 성분들이 있다.
      • 일반형을 제공하기 위해 템플릿을 사용한다.
      • 컨테이너에 들어 있는 데이터에 접근하기 위한 일반화 표현을 제공하기 위해 이터레이터를 사용한다.

  • STL은 알고리즘 라이브러리를 네 그룹으로 나눈다.
  • 처음 세 그룹은 algorithm 헤더 파일, 네 번째 그룹은 numeric 헤더 파일에 정의되어 있다.
    • (1) 변경 불가 시퀀스 연산
      • 어떤 범위에 들어 있는 각 원소에 작용한다.
      • 이 연산은 컨테이너를 변경하지 않는다.
      • find(), for_each()
    • (2) 변경 가능 시퀀스 연산
      • 어떤 범위에 들어 있는 각 원소에 작용한다.
      • 이 연산은 컨테이너를 변경한다.
      • transform(), random_shuffle(), copy()
    • (3) 정렬 및 그와 관련된 연산
      • sort()
    • (4) 일반화한 수치 연산
      • 어떤 범위에 있는 내용들을 더하고, 두 컨테이너의 내적, 부분합, 인접차를 계산하는 등의 함수들이 있다.
      • 일반적으로 이 연산들은 배열의 특성을 가지므로 vector 컨테이너가 가장 잘 어울린다.

  • 이터레이터와 이터레이터 범위를 사용한다.
    • 템플릿 매개변수와 이름을 통해 그 매개변수가 모델링하는 개념을 나타낸다.
    • 따라서 아래 선언에 따르면,
      • 범위 매개변수들이 입력 이터레이터 또는 그 이상이 되어야 하며,
      • 결과를 넣을 위치를 나타내는 이터레이터가 출력 이터레이터 그 이상이 되어야 한다는 사실을 알 수 있다.
template<class InputIterator, class OutputIterator>
OutputIterator copy(InputIterator first, InputIterator last, OutputIterator result);

  • 알고리즘을 분류하는 또 다른 방법은, 알고리즘의 결과가 제자리에 놓이는지, 아니면 새로운 곳에 놓이는지에 따라 분류하는 것이다.
    • (1) 제자리 알고리즘(in-place algorithm)
    • (2) 복사 알고리즘(copying algorithm)

  • 예를 들어, sort()함수는 결과가 오리지널 데이터가 있던 곳에 그대로 위치하기 때문에 제자리 알고리즘(in-place algorithm)이다.
    • 그러나 copy()함수는 결과가 다른 위치에 저장되므로, 복사 알고리즘(copying algorithm)이다.
    • transform()함수는 둘 다 가능하다.
  • 어떤 함수는 두 가지 버전을 갖고 있다.
    • 복사 버전은 이름에 _copy를 붙이는 것이 관행이다.
    • 예를 들어, replace()함수가 있다.
template<class ForwardIterator, class T>
void replace(ForwardIterator first, ForwardIterator last,
    const T& old_value, const T& new_value);
// 제자리 알고리즘.
// old_value의 각 인스턴스를 new_value로 바꾼다. 


template<class InputIterator, class OutputIterator, class T>
OutputIterator replace_copy(InputIterator first, InputIterator last, 
    OutputIterator result,
    const T& old_value, const T& new_value);
// 복사 알고리즘.
// 복사 알고리즘의 관행은,
// 마지막으로 복사된 값 바로 다음 위치를 지시하는 이터레이터를 리턴하는 것이다. 

  • 일부 함수들은 조건부에 따라 동작을 수행하는 버전을 가진다.
    • 함수 이름에 if를 붙이는 것이 관행이다.
template<class ForwardIterator, class Predicate, class T>
void replace(ForwardIterator first, ForwardIterator last,
    Predicate pred, 
    const T& new_value);
// 조건(predicate)은 bool값을 리턴하는 단항 함수이다. 

  • STL과 string 클래스
    • string 클래스는 STL을 염두해 두고 설계되어 있다. 그래서 STL 인터페이스를 사용할 수 있다.
    • 예를 들어, next_permutation() 함수를 사용해서 글자들이 형성하는 순열을 만들 수 있다.
string letters;

cout << "글자를 입력하세요. (quit to quit): ";
while (cin >> letters && letters != "quit")
{
    cout <<  letters << "의 모든 치환들: " << endl;

    sort(letters.begin(), letters.end());
    cout << letters << endl;
    // next_permutation은  더 큰 순열로 재배열을 할 수 있으면 반복하여 구해내는 구조이므로
    // 앞에 이미 큰 원소들이 배치되어 있으면 반복하지 않게 된다.
    // 따라서 오름차순으로 정렬을 해준다. 

    while (next_permutation(letters.begin(), letters.end()))
        cout << letters << endl;
    // next_permutation은 범위에 있는 내용의 고유한 그 다음 순열을 제공한다.

    cout << "다음 글자를 입력하세요. (quit to quit): ";
}

  • STL 메서드와 STL 함수 중에서 어느 것을 사용해야 할까?
    • 일반적으로 메서드가 더 낫다. 그 이유는…
      • 특별한 컨테이너를 위해 최적화되어있기 때문이다.
      • 멤버 함수이기 때문에 템플릿 클래스의 메모리 관리 기능을 이용할 수 있고, 필요한 경우 컨테이너 크기를 조절할 수 있다.
    • 예를 들어, remove() 메서드와 함수를 비교해 볼 수 있겠다.
      • remove 함수는 멤버가 아니라서 크기를 조절할 수 없다.
      • 대신에 남아 있는 원소들을 리스트의 앞쪽에다 놓는다.
      • 그리고 남아 있는 원소들의 새로운 past-the-end을 지시하는 이터레이터를 리턴한다.
      • 이 이터레이터를 사용해서 리스트의 크기를 조절할 수 있다.
// remove 메서드 사용
list<int> li = {1, 4, 1, 4, 4, 4};

li.remove(4);
cout << li.size() << endl;  // 크기가 자동으로 줄어들어서 2


// remove 함수 사용
list<int> li2 = {1, 4, 1, 4, 4, 4};

list<int>::iterator last;
last = remove(li2.begin(), li2.end(), 4);
cout << li2.size() << endl;  // 크기가 그대로 6

li2.erase(last, li2.end());  // 뒤를 지운다. 
cout << li2.size() << endl;  // 크기가 2

  • STL 사용하기 예시 코드
char toLower(char ch)
{
    return tolower(ch);
}

string& ToLower(string& st)
{
    transform(st.begin(), st.end(), st.begin(), toLower);
    return st;
}

void display(const string& s)
{
    cout << s << " ";
}

int main()
{
    // vector 컨테이너: 입력을 저장한다. 
    vector<string> words;
    cout << "단어 입력 (enter quit to quit) :\n";
    string input;
    while (cin >> input && input != "quit")
        words.push_back(input);
    
    cout << "다음과 같은 단어들을 입력하셨습니다. :\n";
    for_each(words.begin(), words.end(), display);
    cout << endl;
    
    // set 컨테이너: 유일하며, 순서대로 저장되는 단어들.
    set<string> wordset;
    transform(words.begin(), words.end(),
        insert_iterator<set<string>>(wordset, wordset.begin()),
        ToLower);
    cout << "\n단어들의 알파벳순 리스트 :\n";
    for_each(wordset.begin(), wordset.end(), display);
    cout << endl;
    
    // map 컨테이너: 키를 단어로 하고, 값을 단어의 빈도로 한다. 
    map<string, int> wordmap;
    set<string>::iterator si;
    for (si = wordset.begin(); si != wordset.end(); si++)
        wordmap[*si] = count(words.begin(), words.end(), *si);
    
    cout << "\n단어별 빈도 :\n";
    for (si = wordset.begin(); si != wordset.end(); si++)
        cout << *si << ": " << wordmap[*si] << endl;
}



기타 라이브러리 #

  • vectorvalarray 그리고 array
    • valarray
      • STL의 일부가 아니다.
      • 자동 크기 조절이 안 된다. 그리고 삽입, 정렬, 검색 등과 같은 것들을 하기 위한 메서드가 없다.
      • 하지만 수학 연산에 대한 명쾌한 표기상의 이점을 가지고 있다.
    • array
      • 기존 배열형을 대체하기 위해 설계되었다.
      • 다양한 STL 메서드들을 지원한다. 따라서 알고리즘을 쉽게 적용할 수 있다.
    • vector
      • 컨테이너 클래스와 알고리즘으로 구성되었다.
      • 정렬, 삽입, 재배치, 검색, 다른 컨테이너로의 데이터 전송 등과같은 컨테이너 지향적인 액티비티를 지원한다.
vector<int> vec = { 1, 2, 5, 4, 3 };
int temp;

int size = vec.size();
valarray<int> valArr(size);
for (int i = 0; i < size; i++)  // vector의 내용을 valarray로 옮긴다.
    valArr[i] = vec[i];

// vector는 정렬 알고리즘 sort()를 사용할 수 있다. 
sort(vec.begin(), vec.end());   

// C++11에서는 이렇게 valarray에 sort()를 사용할 수 있다. 
sort(begin(valArr), end(valArr));  

// valarray는 간단하게 한 문장으로 배열을 연산할 수 있다.
valarray<int> sq_rts(size);
sq_rts = sqrt(valArr);            

valarray<int> results(size);
results = valArr + 2 * sq_rts;

// vector는 이런 방식으로 해야된다.
vector<int> result(size);
transform(vec.begin(), vec.end(), result.begin(), bind1st(plus<int>(), 2));

  • slice() 객체는 배열 인덱스로 사용할 수 있다.
    • 일반적으로 하나의 값이 아니라, 값들로 이루어진 부분 집합을 나타낸다.
    • 세 개의 정수 start, number, stride 값으로 초기화된다.
      • 첫 인덱스인 start부터 시작해서 stride 값을 더하면서 총 number 개의 원소를 뽑아낸다.
    • 이것은 1차원 valarray 객체를 사용해서 2차원 데이터를 나타내는 것을 허용한다.
// valarray를 출력한다.
// cols는 열의 개수이다. 이 개수에 따라 띄어쓴다. 
void show(const valarray<int>& v, int cols)
{
    int lim = v.size();

    for (int i = 0; i < lim; ++i)
    {
        cout.width(3);
        cout << v[i];
        if (i % cols == cols - 1) cout << endl;
        else                      cout << ' ';
    }

    if (lim % cols != 0) cout << endl;
}

int main()
{
    valarray<int> valint = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};  // 4행 3열로 생각한다. 

    cout << "원래의 배열 :\n";
    show(valint, 3); // 3열로 출력한다. 

    valarray<int> vcol(valint[slice(1, 4, 3)]); // 1, 4, 7, 10이며 이것은 두 번째 열이다. 
    cout << "2열을 출력한다. :\n";
    show(vcol, 1);   // 1열로 출력한다. 

    valarray<int> vrow(valint[slice(3, 3, 1)]); // 3, 4, 5이며 이것은 두 번째 행이다. 
    cout << "2행을 출력한다. :\n";
    show(vrow, 3); 

    valint[slice(2, 4, 3)] = 10;                // 2, 5, 8, 11이며 이것은 세 번째 열이다. 
    cout << "3열을 10으로 설정한다. :\n";
    show(valint, 3);

    cout << "1열의 2열과 3열의 합으로 만든다. :\n";
    valint[slice(0, 4, 3)]                      // 0, 3, 6, 9 즉 1열을 2열과 3열의 합으로 만든다. 
        = valarray<int>(valint[slice(1, 4, 3)]) + valarray<int>(valint[slice(2, 4, 3)]);
    show(valint, 3);
}
원래의 배열 :
  0   1   2
  3   4   5
  6   7   8
  9  10  11
2열을 출력한다. :
  1
  4
  7
 10
2행을 출력한다. :
  3   4   5
3열을 10으로 설정한다. :
  0   1  10
  3   4  10
  6   7  10
  9  10  10
1열의 2열과 3열의 합으로 만든다. :
 11   1  10
 14   4  10
 17   7  10
 20  10  10

  • initializer_list 템플릿 (C++11)
    • 컨테이너 클래스가 initializer_list<T> 매개변수를 취하는 생성자를 지녔기 때문에 다음과 같은 초기화가 가능해진다.
    • 만약 클래스가 다양한 크기의 리스트를 처리하지 못할 경우에는 initializer_list 생성자를 제공하는 것은 부적합하다.
vector<double> vec1 {1.2, 3.2, 4.4};  // (O)

// 위의 코드는 다음과 같이 동작하는 것이다. 
vector<double> vec1 ({1.2, 3.2, 4.4});

vector<int> vec2(10)  // 원소 10개를 가진 벡터를 생성한다. 
vector<int> vec3{10}  // 10이라는 원소 1개를 가진 벡터를 생성한다. 

vector<int> vec4{ 1.2 };  // (X) narrowing. 컴파일링 시간 에러 발생. 
vector<double> vec5{ 1 }; // (O) 1은 double인 1.0으로 전환된다. 
class Position
{
private:
    int x, y, z;

public:
    Position(int nx, int ny, int nz) : x(nx), y(ny), z(nz) {}
    
    // 고정된 크기이므로 initializer_list 생성자가 없다. 
};

int main()
{
    Position p{ 1, 2, 3 }; // (O) Position(int, int, int) 생성자를 호출한다. 
}

  • initializer_list 사용 예제
    • begin(), end(), size()를 갖고 있다.
    • 이터레이터형은 상수형이므로 목록 안의 값을 바꿀 수 없다.
double Sum(initializer_list<double> il)
{
    double tot = 0;
    for (auto p = il.begin(); p != il.end(); p++)
        tot += *p;
    return tot;
}

double Average(const initializer_list<double>& ril)
{
    double tot = 0;
    int n = ril.size();
    double ave = 0.0;

    if (n > 0)
    {
        for (auto p = ril.begin(); p != ril.end(); p++)
            tot += *p;
        ave = tot / n;
    }
    return ave;
}

int main()
{
    cout << "목록 1: Sum = " << Sum({ 2,3,4 }) << ", Ave = " << Average({ 2,3,4 }) << '\n';

    initializer_list<double> dl = { 1.1, 2.2, 3.3, 4.4, 5.5 };
    cout << "목록 2: Sum = " << Sum(dl) << ", Ave = " << Average(dl) << '\n';

    dl = { 16.0, 25.0, 36.0, 40.0, 64.0 }; // 대입 가능
    cout << "목록 3: Sum = " << Sum(dl) << ", Ave = " << Average(dl) << '\n';
}