[C++ Primer Plus] Chapter 16. (5) STL - 알고리즘, 기타 라이브러리
Table of Contents
C++ 기초 플러스 책을 읽고 공부한 노트입니다.
알고리즘(algorithm) #
- 알고리즘
- 컨테이너와 함께 사용할 수 있는 멤버가 아닌 함수들이다.
- 알고리즘 함수를 설계하는 주요 두 가지 일반화 성분들이 있다.
- 일반형을 제공하기 위해 템플릿을 사용한다.
- 컨테이너에 들어 있는 데이터에 접근하기 위한 일반화 표현을 제공하기 위해 이터레이터를 사용한다.
- STL은 알고리즘 라이브러리를 네 그룹으로 나눈다.
- 처음 세 그룹은
algorithm
헤더 파일, 네 번째 그룹은numeric
헤더 파일에 정의되어 있다.- (1) 변경 불가 시퀀스 연산
- 어떤 범위에 들어 있는 각 원소에 작용한다.
- 이 연산은 컨테이너를 변경하지 않는다.
find()
,for_each()
- (2) 변경 가능 시퀀스 연산
- 어떤 범위에 들어 있는 각 원소에 작용한다.
- 이 연산은 컨테이너를 변경한다.
transform()
,random_shuffle()
,copy()
- (3) 정렬 및 그와 관련된 연산
sort()
- (4) 일반화한 수치 연산
- 어떤 범위에 있는 내용들을 더하고, 두 컨테이너의 내적, 부분합, 인접차를 계산하는 등의 함수들이 있다.
- 일반적으로 이 연산들은 배열의 특성을 가지므로
vector
컨테이너가 가장 잘 어울린다.
- (1) 변경 불가 시퀀스 연산
- 이터레이터와 이터레이터 범위를 사용한다.
- 템플릿 매개변수와 이름을 통해 그 매개변수가 모델링하는 개념을 나타낸다.
- 따라서 아래 선언에 따르면,
- 범위 매개변수들이 입력 이터레이터 또는 그 이상이 되어야 하며,
- 결과를 넣을 위치를 나타내는 이터레이터가 출력 이터레이터 그 이상이 되어야 한다는 사실을 알 수 있다.
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;
}
기타 라이브러리 #
vector
와valarray
그리고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';
}