본문 바로가기
프로그래밍/C++

C++ ] std::map 자료구조 사용법

by eteo 2023. 9. 23.

 

 

map은 키-값 쌍을 저장하고 검색하는 데 사용되는 자료구조 중 하나로 사용법은 아래와 같다.

 

1. map 헤더파일 포함

 

#include <map>

 

 

 

2. map 객체 생성하기

 

생성시 키와 값의 자료형을 지정해야하는데 개인적으로는 문자열 키와 함수포인터 값을 가지는 map을 자주사용한다.

 

map<string, int> myMap;

 

 

 

3. 원소 추가하기 (insert)

 

원소를 추가할 때는 키-값 쌍을 pair 객체로 전달하여 추가하는데 insert 함수를 사용하는 방법도 있고 아래의 4번을 사용하는 방법도 있다. 각 키는 고유해야 하며, 각 키에 대응하는 값이 하나씩 존재하게 된다.

 

pair 객체를 만들때는 pair<type1, type2>로 생성하거나 make_pair 함수를 사용해도 되고 혹은 {} 중괄호를 사용하는 방법도 있다.

 

또한 map는 중복을 허용하지 않는 자료구조로 insert로 중복키 삽입을 시도하면 추가되지 않는다.

 

또한 insert 함수는 삽입 작업이 성공했는지 실패했는지를 나타내는 pair<iterator, bool>를 반환하는데 여기서 bool 값을 확인해 true이면 새로운 요소가 삽입됐음을, false이면 중복된 키 때문에 삽입이 무시됐음을 알 수 있다.

 

#include <iostream>
#include <map>

using namespace std;

int main() {
        
    map<string, int>myMap;
    myMap.insert(pair<string, int>("apple", 5));
    myMap.insert(make_pair("banana", 3));
    myMap.insert({"orange", 4});    
    
    auto result = myMap.insert({"apple", 10}); // 중복 삽입 시도 -> 추가 되지 않음
    if (!result.second) {
        cout << "오류: 키가 이미 존재합니다!" << endl;
    } else {
        cout << "키 삽입 성공!" << endl;
    }
    
    for(auto &pair : myMap) {
        cout << "Key: " << pair.first << ", Value: " << pair.second << endl;
    }
    return 0;
}

 

 

✔ emplace

emplace 메소드는 emplace_back과 유사한 개념이지만, emplace_back이 주로 std::vector, std::deque, std::list 같은 시퀀스 컨테이너의 끝에 요소를 추가할 때 사용된다면, emplace는 키-값 쌍을 갖는 std::map이나 요소의 유니크한 값을 갖는 std::set과 같은 연관 컨테이너에 요소를 추가할 때 사용된다. insert와 emplace의 관계는 마치 push_back과 emplace_back과 같은데 insert는 컨테이너에 요소를 추가할 때 해당 요소의 복사본을 생성하거나, C++11 이후에는 이동을 사용해 요소를 추가하는 반면, emplace는 전달된 생성에 필요한 인자를 이용해 컨테이너 내에서 객체를 직접 "in-place"로 생성하므로 특히 복사나 이동 생성자의 비용이 큰 객체를 컨테이너에 추가할 때 효율적이다.

 

 

 

4. [키]로 값에 접근하여 원소 추가 또는 수정하기

 

map[키] = 값 형태를 사용하면 해당 키가 이미 map에 존재하는 경우 그 키의 값을 업데이트하고, 키가 존재하지 않는 경우 새로운 키-값 쌍을 map에 추가할 수 있다.

 

myMap["apple"] = 5;  // apple 키에 5 할당
myMap["banana"] = 3; // banana 키에 3 할당
myMap["apple"] = 10; // apple 키의 값을 10으로 수정

 

int-string 페어라면 myMap[10] = "apple" 로 추가 또는 수정한다.

 

 

 

 

5. 원소 삭제하기

 

erase(키) 함수 사용

 

myMap.erase("banana"); // banana 원소 삭제

 

 

 

 

 

6. 반복문 통한 전체 map 탐색

 

map에서 키-값 쌍을 가져올 때는 key는 first로 값은 second로 접근할 수 있다.

 

for (const auto& pair : myMap) {
    cout << pair.first << " = " << pair.second << endl;
}

 

 

 

 

7. map에서 원소 검색하기

 

auto it = myMap.find("apple");
if (it != myMap.end()) {
    int value = it->second;
    cout << "apple의 값: " << value << endl;
} else {
    cout << "apple을 찾을 수 없습니다." << endl;
}

 

 

 

 

8. map의 정렬 순서

 

map은 자동으로 정렬을 유지하는 컨테이너로, 키를 기준으로 오름차순 정렬을 유지한다. 

정렬 기준은 키의 타입에 정의된 비교 연산자(operator <)에 의해 결정되는데 만약 키가 문자열이라면 알파벳 순으로, 정수라면 숫자의 크기 순으로 정렬되는 식이다.

 

때문에 요소의 삽입, 제거가 빈번할 경우 성능이 저하될 수 있으며 정렬할 필요가 없는 경우 map 대신 unordered_map을 사용할 수 있다.

 

#include <iostream>
#include <map>
using namespace std;

int main() {
    map<int, string> myMap;

    // map에 값들을 "무작위" 순서로 삽입
    myMap[5] = "five";
    myMap[2] = "two";
    myMap[3] = "three";
    myMap[1] = "one";
    myMap[4] = "four";

    // map을 순회하며 키와 값을 출력
    for(const auto& pair : myMap) {
        cout << pair.first << ": " << pair.second << endl;
    }

    return 0;
}

 

 

 

 

 

 

9. map 값의 초기화

 

맵에 새로운 키로 접근하면 해당 키-값 쌍이 자동으로 생성되고, 값은 해당 타입의 기본 생성자를 사용하여 초기화된다.

 

따라서 추가적인 초기화 로직 없이 아래 예시와 같이 값을 업데이트해도 올바르게 동작할 것으로 기대할 수 있다. 이는 값의 데이터 타입이 컨테이너인 경우에도 마찬가지이다.

 

#include <iostream>
#include <map>
using namespace std;

int main() {
    map<string, int> myMap;
	// 키가 존재하지 않는 경우, 자동으로 해당 키를 가진 새로운 요소를 생성하고 기본 생성된 값을 반환
    myMap["1번"]++;
    myMap["2번"] += 2;

    for(const auto& pair : myMap) {
        cout << pair.first << ": " << pair.second << endl;
    }

    return 0;
}

 

 

 

 

 

 

10. 다양한 map의 사용예시

 

값이 vector인 예시.

#include <iostream>
#include <map>
using namespace std;

int main() {
    map<string, vector<int>> scores;
    // 키가 맵에 존재하지 않으면, 그 키에 대해 비어 있는 vector<int>가 자동으로 생성되고, 바로 push_back 메소드를 사용해 해당 벡터에 요소를 추가할 수 있다.
    scores["John"].push_back(85);  // "John"의 점수 목록에 85 추가
    scores["Jane"].push_back(90);  // "Jane"의 점수 목록에 90 추가
    
    // "John"의 모든 점수 출력
    for (int &score : scores["John"]) {
        cout << "John's score: " << score << endl;
    }

    return 0;
}

 

 

 

값이 map인 예시.

#include <iostream>
#include <map>
using namespace std;

int main() {
    map<string, map<string, int>> storeInventory;
    // 키가 맵에 존재하지 않으면, 그 키에 대해 비어 있는 map<string,int>가 자동으로 생성되고, 바로 [] 연산자를 사용해 해당 맵에 요소를 추가할 수 있다.
    storeInventory["fruit"]["apple"] = 30;  // 과일 섹션의 사과 수량 설정
    storeInventory["vegetable"]["carrot"] = 20;  // 채소 섹션의 당근 수량 설정
    
    // 과일 섹션의 사과 수량 출력
    cout << "Apples in fruit section: " << storeInventory["fruit"]["apple"] << endl;

    return 0;
}

 

 

 

값이 함수포인터인 예시.

#include <iostream>
#include <map>
#include <functional>

void Function1() {
    cout << "Function1이 호출되었습니다." << endl;
}

void Function2() {
    cout << "Function2가 호출되었습니다." << endl;
}

int main() {

    map<string, function<void()>> functionMap;

    functionMap["Function1"] = Function1;
    functionMap["Function2"] = Function2;

    string key = "Function1";
    auto it = functionMap.find(key);
    if (it != functionMap.end()) {
        functionMap[key](); // 또는 it->second();
    } else {
        cout << "키를 찾을 수 없습니다." << endl;
    }

    return 0;
}

 

 

 

값이 pair인 예시.

 

map이 아닌 pair는 내부적으로 정렬 순서를 유지하지 않는다.

#include <iostream>
#include <map>
#include <algorithm>

using namespace std;

void print1(const string &msg)
{
    cout << "print1 called. msg is " << msg << endl; 
}

void print2(const string &msg)
{
    cout << "print2 called. msg is " << msg << endl; 
}

int main()
{
    map<string, pair<int, void (*)(const string&)>> myDoubleMap;
    
    myDoubleMap["green"] = make_pair(5, print1);
    myDoubleMap["red"] = make_pair(3, print2);
    
    string toFind("red");
    
    auto it = myDoubleMap.find(toFind);
    
    if(it != myDoubleMap.end())
    {
        if(it->second.first = toFind.length())
        {
            it->second.second(toFind);
        }
    }

    return 0;
}

 

 

 

키가 class인 예시.

 

앞에서 설명한 것처럼 map은 내부적으로 < 연산자를 통해 키에 대해 오름차순 정렬된 상태를 유지하므로, 사용자 정의 타입 키를 사용하는 경우 operator<를 재정의 해줘야 한다.

 

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

class Person {
public:
    std::string name;
    int age;

    Person(const std::string& name, int age) : name(name), age(age) {}

    // operator<를 재정의하지 않으면 오류가 남, 재정의를 통해 멤버변수인 이름을 기준으로 정렬.
    bool operator<(const Person& other) const {
        return name < other.name;
    }
};

int main() {
    std::map<Person, int> people;

    people.emplace(Person("Alice", 30), 1);
    people.emplace(Person("Bob", 25), 2);
    people.emplace(Person("Charlie", 35), 3);

    // 출력: Alice, Bob, Charlie
    for (const auto& [person, value] : people) {
        std::cout << person.name << std::endl;
    }

    return 0;
}