Chapter 7 연관 컨테이너 사용하기 USING ASSOCIATIVE CONTAINERS 7 장에서는… 지금까지 순차컨테이너 (sequential container): vector, list 순차컨테이너의 요소들은 우리가 선택한 순서대로 유지된다 7장에서는 순차컨테이너를 이용하는 경우 비효율적인 경우가 있다. 예: 컨테이너 요소에 정수 42가 포함되어있는지 확인하는 문제 방법 1) 컨테이너의 시작부터 끝까지 검사 방법 2) 적당한 순서대로 저장하여 원하는 요소를 효과적으로 찾을 수 있는 알고리즘 이용 효과적인 참조방법을 제공하는 연관 컨테이너: map 2 연관 컨테이너(Associative Containers) 연관 컨테이너의 요소들은 삽입한 순서대로 배열하지 않고, 요소의 값에 따라 삽입 순서를 자동적으로 조정한다. 따라서, 특정 요소들을 더 빨리 검색할 수 있다. Maps: 빠른 검색을 위한 연관 컨테이너 연관 자료구조에는 키-값 쌍(key-value pair)으로 저장 연관 배열(associative array) 값과 키를 연결시켜서, 키(key) 에 의해 빠르게 삽입, 탐색 효과적인 검색을 위해 사용하는 컨테이너의 요소 3 map map 을 정의하는 형식: map<key_type, value_type> var_name; map의 각 요소은 pair 타입 pair 타입: pair <key> <value> first second Austria 43 Korea 82 U.S.A 1 first와 second를 멤버로 갖는 구조체(struct) 타입 pair 클래스는 여러 타입의 값을 담을 수 있다. 따라서, first와 second 의 데이터 멤버를 명시해야 한다. pair<const key_type, value_type> 키는 항상 const이기 때문에 해당하는 값은 바꿀 수 없다. #include <map> 4 순차 컨테이너와 연관 컨테이너 map과 vector의 기본적인 차이점: 인덱스 대부분의 경우, map은 vector처럼 동작한다 vector: 인덱스가 정수이다. v[0], v[1], v[2], … map: 다른 것들과 순서를 비교할 수 있는 것이라면 어떤 타입도 가능하다. string 일 수 있다. 연관 컨테이너의 중요한 특징은 자체 순서 (self-ordering)를 유지시킨다. 입력 순서와 상관없이 키 값의 순서에 따라 저장된다. 따라서, 순서를 변경하는 작업은 할 수 없다. 5 예 각국의 국제전화 코드번호 저장할 정보의 키와 값 쌍: <국가이름, 국가코드번호> map<string, int> m; m[“Canada”] = 1; m[“Austria”] = 43; pair<const string, int> e1(“Czech”, 42); m.insert(e1); cout << e1.first << “ ” << e1.second << endl; … <key> <value> first second Austria 43 Canada 1 Czech 42 Egypt 20 Japan 81 Korea 82 Spain 34 U.S.A 1 6 Map 반복자 Map iterator: 순차적으로(sequentially) Map에 저장된 내용을 접근하도록 이용 가능 begin() end() map 반복자는 pair를 가리킨다: Map에 저장된 pair의 key 부분과 value 부분을 참조 it->first it->second 삽입된 순서와 상관없이 키 값에 따라 순서를 유지하고 있어서 정렬되어 출력된다. for(map<string, int>:: const_iterator it = m.begin(); it != m.end(); ++it) cout << it-> first << “ ” << it->second << endl; 7 Map 멤버함수 insert() : 요소 삽입 find() : 키를 이용한 검색 erase() : 요소 삭제 begin() end() size() clear() … 참조사이트: http://msdn.microsoft.com/ko-kr/library/xdayte4c(v=VS.90).aspx 8 연관컨테이너에 삽입 방법 연산자 []를 이용하는 방법 m1[1] = 10; m1[5]; m1[1] = 11; map <int, int> m1; // 디폴트 값 0으로 초기화 됨; m1[5]=0; // 이미 있는 요소일 때: m[1]의 값을 변경함 insert() 멤버함수를 이용하는 방법 pair 타입을 이용 1) pair<const int, int> e1(4, 40); m1.insert(e1); 2) m1.insert(pair<const int, int> (8, 80) ); 9 #include <map> … int main( ) { map <int, int> m1; map <int, int> :: iterator pIter; // Insert a data value of 10 with a key of 1 into a map // using the operator[] member function m1[ 1 ] = 10; // Compare other ways to insert objects into a map m1.insert ( pair <const int, int> ( 3, 30 ) ); m1.insert ( pair <const int, int> ( 2, 20 ) ); for ( pIter = m1.begin( ) ; pIter != m1.end( ) ; pIter++ ) cout << pIter -> first << ": " << pIter -> second << endl; // If the key already exists, operator[] // changes the value of the datum in the element m1[ 2 ] = 40; // operator[] will also insert the value of the data // type's default constructor if the value is unspecified m1[5]; // m1[5]=0; } for ( pIter = m1.begin( ) ; pIter != m1.end( ) ; pIter++ ) cout << pIter -> first <<“: " << pIter -> second << endl; 출력결과: 1: 10 2: 20 3: 30 1: 2: 3: 5: 10 40 30 0 operator[]와 insert() 함수 비교 operator[]를 이용해서 삽입한 경우, 삽입 후 이미 존재하는 요소(element)가 변경되었는지, 새로운 요소를 삽입했는지를 알 수 없다. insert() 멤버함수를 이용해서 삽입을 시도한 경우, 삽입하기 전에, key가 이미 존재하는를 알려준다. return_type은 map 반복자와 bool로 된 pair 타입이다. pair< map<int,int>::iterator, bool > pr; 이미 존재할 때: map에서 해당요소의 pair를 참조하는 반복자와 false를 리턴한다. 존재하지 않을 때: map에 pair를 새로 추가하고, 추가된 pair를 참조하는 반복자와 true를 리턴한다. pr = m1.insert( make_pair(1, 11) ); if( pr.second == true ) // 존재하지 않을 때 … else … 11 insert() 함수 사용 예 #include <map> #include <iostream> using namespace std; int main( ) { map <int, int> m; m.insert ( make_pair ( 1, 100 ) ); pair< map<int,int>::iterator, bool > pr; pr = m.insert ( make_pair ( 1, 10 ) ); if( pr.second == true ) { cout << "The element was inserted in m successfully." << endl; } else { cout << "Key number already exists in m\n" << "with an associated value = " << ( pr.first ) -> second // 저장된 값 << "." << endl; } } 12 operator [] m[Key] = DataValue ; m[1] = 10; cout << m[1]; // 10 출력 cout << m[5]; // 0 출력 연관컨테이너에서 Key를 탐색한다. 있으면, DataValue로 Key와 연관된 값을 변경한다. 없으면, Key와 DataValue 로 된 새로운 pair를 삽입한다. m[s]; key에 s 를 포함한 pair 를 연관컨테이너에서 탐색한다. 컨테이너에 있으면, pair에 있는 s와 연관된 값에 대한 참조(reference)를 리턴한다. 연관 컨테이너에 없으면, 1) key를 s로 하고, s와 연관된 값을 디폴트 초기값(int, double 은 0으로 초기화)으로 하는 pair를 생성한다. 2) 컨테이너의 적절한 위치에 pair를 삽입한다. 3) 저장된 새로운 pair의 값에 대한 참조를 리턴한다. 13 find() 멤버 함수 find 함수: Map에 Key 값이 존재하는지를 탐색한다 m.find( key ); 탐색할 Key가 map에 저장되어 있으면, 해당 pair를 참조하는 반복자를 리턴한다. 없으면, m.end()를 리턴한다. map <int, int> m; map <int, int> :: const_iterator m_iter; m.insert ( make_pair ( 1, 10 ) ); m.insert ( make_pair ( 2, 20 ) ); m.insert ( make_pair ( 3, 30 ) ); 출력 결과: The element of map m with a key of 2 is: 20 The map m doesn't have an element with a key of 4. m_iter = m.find( 2 ); if ( m_iter != m.end( ) ) cout << "The element of map m with a key of 2 is: " << m_iter -> second << endl; // If no match is found for the key, end( ) is returned m_iter = m.find( 4 ); if ( m_iter == m.end( ) ) // NOT FOUND cout << "The map m doesn't have an element with a key of 4." << endl; else // FOUND cout << "The element of map m with a key of 4 is: " << m_iter -> second << endl; 14 erase() 멤버함수 erase()함수: Map에서 요소를 삭제하는 함수 void erase(iterator); // iterator가 참조하는 요소를 삭제 void erase(iterator first, iterator last); // first부터 last 앞까지 삭제 size_type erase(Key); // 해당 Key값의 요소를 삭제; 삭제된 개수를 리턴 map<int, int> m; map<int, int>::iterator pIter, Iter1, Iter2; int i; map<int, int>::size_type n; for (i = 1; i < 10; i++) m.insert(make_pair(i, i)); Iter1 = ++m.begin(); m.erase(Iter1); n = m.erase ( 3 ); 출력 결과: Iter1 = ++m.begin(); Iter2 = --m.end(); m.erase(Iter1, Iter2); 1 4 5 6 7 8 9 1 9 for (pIter = m.begin(); pIter != m.end(); pIter++) cout << " " << pIter->second; cout << endl; for (pIter = m.begin(); pIter != m.end(); pIter++) cout << " " << pIter->second; cout << endl; 15 7.2 단어개수 세기 입력에 각 단어가 몇 번씩 나오는지를 세는 프로그램 벡터를 이용했을 때.. 꽤 복잡하네. struct word_frequency { string word; int freq; }; vector<word_frequency> words; int main() { string s; while(cin>>s) { s가 vector 속에 있는지 찾아보고, 있다면 그것에 해당하는 freq를 하나 증가시키고 없다면 새로 요소를 추가하고 … } } 결과를 출력한다. 16 단어개수 세기 – map 이용 map을 사용하면… 신기할 정도로 간단하네 #include <iostream> #include <map> #include <string> using namespace std; int main() { string s; map<string, int> counters; // store each word and an associated counter // read the input, keeping track of each word and how often we see it while (cin >> s) ++counters[s]; // write the words and associated counts for (map<string, int>::const_iterator it = counters.begin(); it != counters.end(); ++it) { cout << it->first << "\t" << it->second << endl; } return 0; } 17 단어 개수 세기: 분석 map<string, int> counters; counters 를 map으로 정의. “string에서 int로의 map” 키: string 타입, 연관된 값: int 타입 ++counters[s]; 키로 읽어들인 단어 s를 counters에서 찾는다 counters[s]는 s에 저장된 string에 연관된 정수를 리턴한다. ++ 를 사용하여, 저장된 정수 값을 증가시킨다. 값지정-초기화 (value-initialzed) int와 같은 단순한 타입의 경우에는 0으로 초기화 된다. 처음 나타난 단어는 0으로 초기화 된 상태에서, 증가시키므로, 1 cout << it->first << "\t" << it->second << endl; it->first : 현재 요소의 키 it->second : 키와 연관된 값 18 7.3 교차-참조 테이블 생성 단어가 입력의 어느 라인에서 나타나는지를 저장하는 교차참조 테이블 (cross-reference table)을 생성하는 프로그램 한번에 한 라인을 읽어 들여서, 라인번호와 연관 짓는다. 단어 대신 라인을 읽으므로, 단어별로 분리하는 작업이 필요 5.6절의 split() 함수 이용 교차-참조함수 xref()에 대한 매개변수로 split함수를 이용 라인 상에서 단어 찾는 방식을 다른 함수로 바꿀 수 있도록. string에서 vector<int>로의 map 을 이용: 키: 개별 단어 연관된 값: 단어가 나타난 라인 번호들을 저장 map<string, vector<int> > ret; 19 // find all the lines that refer to each word in the input map<string, vector<int> > xref(istream& in, vector<string> find_words(const string&) = split) { string line; 입력 예: int line_number = 0; aaa bbb ccc map<string, vector<int> > ret; ddd aaa ccc eee aaa // read the next line 저장 예: while (getline(in, line)) { aaa 123 ++line_number; // break the input line into words vector<string> words = find_words(line); bbb 1 ccc 12 ddd 2 eee 3 // remember that each word occurs on the current line for (vector<string>::const_iterator it = words.begin(); it != words.end(); ++it) ret[*it].push_back(line_number); } } return ret; 20 xref() 함수 분석 map<string, vector<int> > ret; 리턴 타입과 지역변수 선언에서 > > 를 사용. (>> 아님) 컴파일러가 이해하기 위해 공백(space)가 필요하다. >>연산자와 구분할 수 있도록 !! xref(istream& in, vector<string> find_words(const string&) = split) 인자 목록에서 find_word() 함수를 매개변수로 정의하고 있다. 기본인자(default argument)를 지정: = split 호출하는 곳에서 매개변수를 생략하면, 기본인자를 이용 매개변수를 명시한다면, 그 인자를 사용 xref(cin); // 입력스트림에서 단어들을 찾기 위해 split함수를 이용 xref(cin, find_urls); // ” find_urls함수를 이용 21 xref() 함수 분석 find_words() 함수 split 함수일 수 도 있고, string 인자를 받고 vector<string>을 리턴하는 다른 함수도 가능하다. split함수: line을 각 단어별로 쪼개는 함수 (책 p152 이용) ret[*it].push_back(line_number); words의 각 요소를 순차적으로 순회하면서, 단어에 해당하는 라인번호를 벡터에 추가. it: 벡터 words의 한 요소. *it: 단어 for (vector<string>::const_iterator it = words.begin(); it != words.end(); ++it) ret[*it].push_back(line_number); // string s=*it; // ret[s].push_bak(line_number); 이 처음 나오는 단어라면, vector<int>는 값지정 초기화 된다. 즉, 빈 벡터가 생성된다. 22 main() 함수: 간단한 출력형식으로 int main() { // call `xref' using `split' by default map<string, vector<int> > ret = xref(cin); // write the results for (map<string, vector<int> >::const_iterator it = ret.begin(); it != ret.end(); ++it) { // write the word cout << it->first << “: "; // followed by one or more line numbers for(vector<int>::const_iterator line_it = it->second.begin(); line_it != it->second.end(); ++line_it) { cout << *line_it << “ ”; 입력 예: aaa bbb ccc ddd aaa ccc eee aaa 저장 예: aaa 123 bbb 1 ccc 12 ddd 2 eee 3 // write a new line to separate each word from the next cout << endl; } } return 0; 출력 예: aaa: 1 2 3 bbb: 1 ccc: 1 2 ddd: 2 eee: 3 23 main() 함수: int main() { // call `xref' using `split' by default map<string, vector<int> > ret = xref(cin); } // write the results for (map<string, vector<int> >::const_iterator it = ret.begin(); it != ret.end(); ++it) { 출력 예: // write the word cout << it->first << " occurs on line(s): "; aaa occurs on line(s): 1, 2, 3 bbb occurs on line(s): 1 // followed by one or more line numbers vector<int>::const_iterator line_it = it->second.begin(); ccc occurs on line(s): 1, 2 cout << *line_it; // write the first line number ddd occurs on line(s): 2 eee occurs on line(s): 3 ++line_it; // write the rest of the line numbers, if any while (line_it != it->second.end()) { cout << ", " << *line_it; ++line_it; } // write a new line to separate each word from the next cout << endl; } return 0; 24