PHpullh

C++ · 심층 가이드

C++ 컬렉션 완전 정리

vector와 map을 STL 알고리즘으로 다루는 20개 예제로 변환과 필터, 그룹핑, 슬라이딩 윈도우를 반복자 무효화 함정까지 짚어가며 익힙니다.

주제 20개 · 예제 코드 포함 · 최종 수정 2026-09-03 · 작성 pullh 편집팀

STL은 컨테이너와 알고리즘을 반복자라는 얇은 규약으로 이어 붙인 구조입니다. 덕분에 같은 std::sort가 배열에도 vector에도 붙지만, 대신 대부분의 알고리즘이 원소를 지우거나 컨테이너 크기를 바꾸지 못합니다. 파이썬이나 자바스크립트의 리스트 메서드에 익숙하다면 이 분리가 가장 낯선 부분입니다. 컨테이너 선택도 성능의 큰 축이라, 어떤 자료구조를 고르느냐가 코드 모양보다 먼저 결정됩니다.

map 변환과 filter 필터링, reduce 합계는 각각 transform, copy_if, accumulate로 옮겨집니다. 이 셋을 익히면 groupBy 구현과 빈도표 만들기는 결국 맵에 누적하는 같은 패턴임이 보입니다. 스택 구현과 큐 구현은 컨테이너 어댑터가 기존 컨테이너를 감싸는 방식이라 따로 외울 것이 적고, sliding window와 chunk 나누기는 인덱스 대신 반복자 구간으로 생각하는 연습에 좋습니다.

지우기에서 한 번은 반드시 걸립니다. std::remove는 원소를 삭제하지 않고 남길 것들을 앞으로 몰아 새 끝 반복자를 돌려줄 뿐이어서, erase를 함께 호출하지 않으면 크기가 그대로입니다. C++20부터는 std::erase_if로 한 번에 처리할 수 있습니다. 또 하나, vector에 원소를 추가하다 재할당이 일어나면 기존 반복자와 포인터가 전부 무효가 되고, unordered_map의 operator[]는 조회만 하려던 키를 조용히 삽입합니다. 읽기만 할 때는 find나 at을 씁니다.

01map 변환

std::transform이 items의 각 원소에 람다를 적용하고, 출력 반복자로 같은 시작 위치를 넘겨 결과를 제자리에 덮어씁니다. 원본을 유지할 필요가 없을 때 메모리를 아끼는 방식입니다. 단항 형태의 transform은 입력과 출력 범위가 완전히 겹치는 경우를 허용하므로 이런 제자리 변환이 규격상 안전합니다.

C++ code

// map 변환
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> items = {1, 2, 3, 1};
    std::transform(items.begin(), items.end(), items.begin(), [](int value) { return value * 2; });
    std::cout << items[0] << "\n";
}
알아두면 좋은 점

C++20의 std::ranges::transform은 컨테이너를 그대로 받아 반복자 쌍을 잘못 조합할 여지를 없애고, 결과가 끝난 위치도 반환값으로 알려 줍니다.

자주 하는 실수

출력 반복자를 입력보다 뒤쪽 위치로 겹쳐 주면 아직 읽지 않은 원소를 먼저 덮어써 결과가 망가지며, 시작 위치가 완전히 같을 때만 안전합니다.

02filter 필터링

std::set<std::string>에 네 개를 적었지만 cpp가 겹쳐 실제 크기는 3이 됩니다. 조건에 맞는 항목만 남기거나 걸러 낼 때 자료 구조 자체가 걸러 주는 셈입니다. set은 해시가 아니라 operator<로 같음을 판정해서, 두 값이 서로 작지 않으면 같은 것으로 취급하고 삽입과 조회는 로그 시간이 듭니다.

C++ code

// filter 필터링
#include <iostream>
#include <set>
#include <string>

int main() {
    std::set<std::string> tags = {"cpp", "stl", "cpp", "rust"};
    std::cout << tags.size() << "\n";
}
알아두면 좋은 점

tags.insert(...)는 삽입 성공 여부를 반환된 쌍의 second로 알려 주므로, 중복이었는지를 따로 조회하지 않고도 알 수 있습니다.

자주 하는 실수

조건에 맞지 않는 항목을 순회 중에 erase하면 그 반복자가 무효가 되므로, 반환값을 다시 받거나 C++20의 std::erase_if를 써야 합니다.

03reduce 합계

각 원소에 2를 곱해 되쓰고 첫 값을 출력합니다. 여러 값을 하나로 접어 내는 계산도 이렇게 범위와 함수를 넘기는 형태를 따릅니다. 합계라면 <numeric>의 std::accumulate를 쓰되, 초기값의 타입이 결과 타입을 정하므로 0 대신 0LL이나 0.0을 넘기지 않으면 결과가 잘려 나갑니다.

C++ code

// reduce 합계
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> items = {1, 2, 3, 3};
    std::transform(items.begin(), items.end(), items.begin(), [](int value) { return value * 2; });
    std::cout << items[0] << "\n";
}
알아두면 좋은 점

std::reduce는 실행 정책을 받아 병렬로 접을 수 있지만 결합 순서를 정하지 않아, 부동소수 합계는 실행마다 결과가 미세하게 달라질 수 있습니다.

자주 하는 실수

누산 도중 중간값이 결과 타입 범위를 넘어서면 부호 있는 정수에서는 정의되지 않은 동작이므로, 평균을 구할 때는 합계 타입을 먼저 넓혀야 합니다.

04첫 요소 찾기

코드는 set의 원소 개수만 세지만, 이 컨테이너는 값을 정렬 상태로 유지하기 때문에 *tags.begin()이 곧 사전순으로 가장 앞선 항목이 됩니다. 조건을 만족하는 첫 항목을 찾는다면 목적에 맞는 도구가 따로 있습니다. std::find_if는 첫 일치에서 탐색을 멈추고, 없으면 끝 반복자를 돌려주므로 역참조 전에 비교해야 합니다.

C++ code

// 첫 요소 찾기
#include <iostream>
#include <set>
#include <string>

int main() {
    std::set<std::string> tags = {"cpp", "stl", "cpp", "rust"};
    std::cout << tags.size() << "\n";
}
알아두면 좋은 점

정렬된 std::set에서는 lower_bound로 기준값 이상인 첫 원소를 로그 시간에 찾을 수 있어, 전체를 훑는 find_if보다 유리합니다.

자주 하는 실수

std::find_if가 돌려준 끝 반복자를 검사 없이 역참조하면 존재하지 않는 원소를 읽는 것이라, 값이 이상해지는 정도로 끝나지 않습니다.

05Set 중복 제거

std::vector에 값을 그대로 두고 변환만 하므로 중복은 남아 있습니다. 중복을 없애려면 std::set에 넣거나 정렬 후 std::unique를 부르는 두 갈래가 있습니다. std::unique는 인접한 중복만 접어 내고 컨테이너 크기는 줄이지 않으므로, 반환된 반복자를 erase에 넘겨야 실제로 지워집니다.

C++ code

// Set 중복 제거
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> items = {1, 2, 3, 5};
    std::transform(items.begin(), items.end(), items.begin(), [](int value) { return value * 2; });
    std::cout << items[0] << "\n";
}
알아두면 좋은 점

std::unique는 인접한 중복만 접어 내므로 반드시 정렬을 먼저 해야 하며, 그러지 않으면 서로 떨어져 있는 같은 값들이 그대로 남습니다.

자주 하는 실수

erase에 unique의 반환 반복자만 넘기고 끝 반복자를 빠뜨리면 원소 하나만 지워져, 뒤쪽 찌꺼기가 그대로 남은 채 크기만 하나 줄어듭니다.

06단어 수 세기

set은 같은 값을 두 번 담지 않아 size()가 서로 다른 항목의 개수를 알려 줍니다. 어떤 항목이 있었는지만 알면 될 때 적합합니다. 반면 각 항목이 몇 번 나왔는지가 필요하다면 std::map<std::string, int>에 ++counts[word]를 쓰는데, 이 첨자 연산은 없는 키를 0으로 만들어 새로 삽입한다는 점을 알아 두면 좋습니다.

C++ code

// 단어 수 세기
#include <iostream>
#include <set>
#include <string>

int main() {
    std::set<std::string> tags = {"cpp", "stl", "cpp", "rust"};
    std::cout << tags.size() << "\n";
}
알아두면 좋은 점

값이 있는지만 확인할 때는 counts[word] 대신 counts.find(word)를 써야, 조회만 했는데 원소가 늘어나는 일을 피할 수 있습니다.

자주 하는 실수

const 참조로 받은 맵에는 operator[]를 쓸 수 없어 컴파일 오류가 나는데, 이 연산자가 필요하면 삽입까지 하기 때문입니다.

07스택 구현

std::vector를 만들어 원소를 일괄 변환하는 코드입니다. 같은 벡터를 후입선출로 쓰려면 push_back과 back, pop_back 세 가지면 충분합니다. pop_back은 값을 돌려주지 않으므로 꺼내 쓰려면 back()으로 먼저 읽어야 하고, push_back이 재할당을 일으키면 기존 반복자와 참조는 모두 무효가 됩니다.

C++ code

// 스택 구현
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> items = {1, 2, 3, 7};
    std::transform(items.begin(), items.end(), items.begin(), [](int value) { return value * 2; });
    std::cout << items[0] << "\n";
}
알아두면 좋은 점

std::stack은 기존 컨테이너를 감싸 중간 접근을 막아 주는 어댑터라, 후입선출 규칙을 타입으로 못 박고 싶을 때 유용합니다.

자주 하는 실수

back()으로 참조를 받아 둔 뒤 push_back을 부르면 재할당 때 그 참조가 무효가 되므로, 값을 복사해 두고 나서 넣어야 합니다.

08큐 구현

std::set은 정렬된 순서로 원소를 유지할 뿐 넣은 순서를 기억하지 않아, 먼저 들어온 것을 먼저 꺼내는 용도와는 맞지 않습니다. 선입선출이 필요하면 컨테이너를 바꿔야 합니다. std::queue는 기본적으로 std::deque 위에 얹히며, 앞쪽 제거가 상수 시간이라 vector의 erase(begin())보다 유리합니다.

C++ code

// 큐 구현
#include <iostream>
#include <set>
#include <string>

int main() {
    std::set<std::string> tags = {"cpp", "stl", "cpp", "rust"};
    std::cout << tags.size() << "\n";
}
알아두면 좋은 점

std::deque는 원소가 여러 블록에 나뉘어 있어 앞뒤 삽입이 모두 상수 시간이지만, 전체가 연속이라고 가정하고 포인터 산술을 하면 안 됩니다.

자주 하는 실수

std::queue의 pop()은 값을 돌려주지 않아 front()로 먼저 읽어야 하고, 빈 큐에서 둘 중 무엇을 불러도 정의되지 않은 동작이 됩니다.

092차원 배열 합계

여기 쓰인 std::vector<int>는 원소가 메모리에 연속으로 놓이는 일차원 구조입니다. 행과 열을 다루려면 std::vector<std::vector<int>>를 쓰거나, 하나의 벡터에 row * cols + col로 인덱스를 계산해 담습니다. 후자는 메모리가 한 덩어리로 붙어 있어 캐시 지역성이 좋고 할당 횟수도 한 번으로 줄어듭니다.

C++ code

// 2차원 배열 합계
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> items = {1, 2, 3, 9};
    std::transform(items.begin(), items.end(), items.begin(), [](int value) { return value * 2; });
    std::cout << items[0] << "\n";
}
알아두면 좋은 점

행 우선으로 담은 벡터를 열 방향으로 훑으면 캐시 라인을 매번 새로 읽게 되므로, 반복문 순서를 저장 순서에 맞추는 것만으로 속도가 크게 달라집니다.

자주 하는 실수

std::vector<std::vector<int>>는 행마다 길이가 달라도 컴파일되므로, 직사각형이라고 가정하고 인덱스를 돌리면 범위를 벗어난 접근이 생깁니다.

10groupBy 구현

모든 원소에 같은 변환을 적용하는 코드로, 항목별로 다른 자리에 나눠 담는 처리와는 방향이 다릅니다. 키별로 묶으려면 std::map<Key, std::vector<Value>>에 groups[key].push_back(item)을 반복하는 형태가 흔합니다. map의 첨자 연산은 키가 없으면 빈 벡터를 만들어 주므로 존재 검사를 따로 넣지 않아도 됩니다.

C++ code

// groupBy 구현
#include <iostream>
#include <set>
#include <string>

int main() {
    std::set<std::string> tags = {"cpp", "stl", "cpp", "rust"};
    std::cout << tags.size() << "\n";
}
알아두면 좋은 점

묶은 결과를 키 순으로 볼 필요가 없다면 std::unordered_map으로 평균 상수 시간에 넣을 수 있고, 각 그룹 안의 순서는 넣은 순서 그대로 유지됩니다.

자주 하는 실수

groups[key]가 돌려준 벡터의 참조를 들고 다른 키를 계속 추가하면, unordered_map에서는 재해시로 반복자가 무효가 된다는 점을 함께 기억해야 합니다.

11flatten 펼치기

std::transform은 입력 하나에 출력 하나를 대응시키므로 원소 수가 그대로 유지됩니다. 중첩된 구조를 한 겹 펼치려면 개수가 달라져야 하고, 그때는 출력 반복자를 다른 컨테이너로 돌려야 합니다. std::back_inserter를 쓰면 목적지 크기를 미리 맞추지 않아도 되며, 그러지 않고 빈 벡터에 그대로 쓰면 범위를 벗어난 접근이 됩니다.

C++ code

// flatten 펼치기
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> items = {1, 2, 3, 11};
    std::transform(items.begin(), items.end(), items.begin(), [](int value) { return value * 2; });
    std::cout << items[0] << "\n";
}
알아두면 좋은 점

결과 개수를 미리 알 수 있다면 목적지 벡터에 reserve를 먼저 해 두어야, back_inserter가 넣는 도중 반복해서 재할당하는 비용을 피할 수 있습니다.

자주 하는 실수

std::back_inserter는 원소를 하나씩 push_back하므로, 대상 컨테이너의 반복자를 미리 저장해 두었다면 작업 도중 모두 무효가 됩니다.

12zip 묶기

이 코드는 컨테이너 하나만 다루며, 원소 수를 세어 출력합니다. 두 컨테이너를 나란히 짝지으려면 짧은 쪽 길이에 맞춰 인덱스를 함께 돌리거나 반복자 둘을 같이 전진시켜야 합니다. 이항 형태의 std::transform은 두 입력 범위를 받아 짝지을 수 있는데, 두 번째 범위의 길이를 검사하지 않으므로 짧은 쪽 길이는 호출자가 보장해야 합니다.

C++ code

// zip 묶기
#include <iostream>
#include <set>
#include <string>

int main() {
    std::set<std::string> tags = {"cpp", "stl", "cpp", "rust"};
    std::cout << tags.size() << "\n";
}
알아두면 좋은 점

C++23의 std::views::zip은 두 범위를 짝지어 짧은 쪽 길이에 자동으로 맞춰 주므로, 길이를 손으로 계산해 맞추는 코드가 사라집니다.

자주 하는 실수

두 컨테이너를 인덱스로 함께 돌면서 한쪽 크기만 확인하면 다른 쪽에서 범위를 넘게 되므로, std::min으로 짧은 길이를 먼저 구해야 합니다.

13chunk 나누기

set에 담긴 항목 수를 확인하는 코드지만, 자료를 일정 크기로 잘라 처리할 때는 먼저 전체 개수를 아는 일이 출발점이 됩니다. set은 임의 접근을 지원하지 않아 begin() + n으로 건너뛸 수 없고 std::advance나 std::next로 한 칸씩 이동해야 하므로, 구간을 잘라 쓸 일이 많다면 vector가 낫습니다.

C++ code

// chunk 나누기
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> items = {1, 2, 3, 13};
    std::transform(items.begin(), items.end(), items.begin(), [](int value) { return value * 2; });
    std::cout << items[0] << "\n";
}
알아두면 좋은 점

std::next(it, n)은 임의 접근 반복자에서는 상수 시간이지만, set처럼 양방향 반복자에서는 n에 비례하는 시간이 듭니다.

자주 하는 실수

구간을 자를 때 마지막 덩어리가 정해진 크기보다 짧다는 점을 잊고 일정 크기만큼 전진시키면, 끝 반복자를 지나쳐 정의되지 않은 동작이 됩니다.

14key 기준 정렬

std::set은 저장할 때부터 비교자에 따라 정렬된 상태를 유지하므로 순회하면 항상 정렬된 순서로 나옵니다. 다른 기준으로 줄을 세우려면 비교자를 타입 인자로 바꿔야 합니다. 원소 자체가 const라 넣은 뒤 키를 고칠 수 없고, 정렬 기준을 바꾸려면 지웠다가 다시 넣거나 vector에 담아 std::sort를 쓰는 편이 간단합니다.

C++ code

// key 기준 정렬
#include <iostream>
#include <set>
#include <string>

int main() {
    std::set<std::string> tags = {"cpp", "stl", "cpp", "rust"};
    std::cout << tags.size() << "\n";
}
알아두면 좋은 점

std::set의 비교자는 타입의 일부라, 비교자가 다른 두 집합은 원소 타입이 같아도 서로 대입할 수 없는 별개의 타입이 됩니다.

자주 하는 실수

원소를 const_cast로 고쳐 정렬 기준을 바꾸면 트리 구조가 실제 순서와 어긋나, 이후의 조회가 원소를 못 찾는 방식으로 조용히 실패합니다.

15빈도표 만들기

값을 두 배로 바꾸는 이 변환은 원소마다 한 번씩 호출되며, 같은 자리 수를 유지합니다. 어떤 값이 몇 번 나오는지 세려면 값을 키로 삼는 std::map이나 std::unordered_map이 필요합니다. unordered_map은 평균 상수 시간이지만 원소를 넣다가 재해시가 일어나면 기존 반복자가 무효가 되며, 참조와 포인터는 그대로 유효합니다.

C++ code

// 빈도표 만들기
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> items = {1, 2, 3, 15};
    std::transform(items.begin(), items.end(), items.begin(), [](int value) { return value * 2; });
    std::cout << items[0] << "\n";
}
알아두면 좋은 점

빈도표에서 상위 항목만 뽑으려면 원소를 벡터로 옮긴 뒤 std::partial_sort로 필요한 개수만 정리하는 편이 전체 정렬보다 저렴합니다.

자주 하는 실수

++counts[value]는 세는 자리에서는 편리하지만, 같은 표현을 조회 전용 함수에 그대로 쓰면 물어볼 때마다 표가 계속 커집니다.

16대소문자 무시 중복 제거

set은 기본 비교자로 문자열을 그대로 비교하므로 Cpp와 cpp는 서로 다른 항목으로 남습니다. 대소문자를 무시하려면 std::set<std::string, CaseInsensitiveLess>처럼 비교자를 직접 넘겨야 합니다. 이때 비교자는 엄격 약순서를 지켜야 하고, 대소문자 변환은 로케일에 따라 결과가 달라질 수 있습니다.

C++ code

// 대소문자 무시 중복 제거
#include <iostream>
#include <set>
#include <string>

int main() {
    std::set<std::string> tags = {"cpp", "stl", "cpp", "rust"};
    std::cout << tags.size() << "\n";
}
알아두면 좋은 점

std::tolower에 char를 그대로 넘기면 음수 값에서 정의되지 않은 동작이 되므로, unsigned char로 변환한 뒤 넘겨야 합니다.

자주 하는 실수

비교자에서 대소문자를 무시하도록 만들면 Cpp와 cpp가 같은 원소로 취급되어, 나중에 넣은 쪽의 원래 표기가 조용히 버려집니다.

17sliding window

std::transform은 원소를 하나씩 독립적으로 처리해 이웃한 값을 함께 보지 않습니다. 연속한 구간을 훑어야 한다면 반복자 두 개를 함께 옮기거나 인덱스로 구간을 잡습니다. 이웃한 두 원소만 비교하면 되는 경우에는 <numeric>의 std::adjacent_difference나 std::adjacent_find로 반복문을 줄일 수 있습니다.

C++ code

// sliding window
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> items = {1, 2, 3, 17};
    std::transform(items.begin(), items.end(), items.begin(), [](int value) { return value * 2; });
    std::cout << items[0] << "\n";
}
알아두면 좋은 점

합계를 구하는 창이라면 한 칸 움직일 때 빠지는 값을 빼고 새로 들어오는 값만 더해, 창 크기와 무관하게 한 칸당 상수 시간으로 끝낼 수 있습니다.

자주 하는 실수

창 크기가 원소 수보다 큰 경우를 검사하지 않으면 시작 반복자가 끝을 넘어서 전진하게 되어, 그 자체로 정의되지 않은 동작이 됩니다.

18pair 목록을 맵으로 변환

std::set은 값 하나만 담아 중복을 없애지만, 키와 값을 짝지어 저장하려면 std::map으로 옮겨야 합니다. std::vector<std::pair<K, V>>를 map의 범위 생성자에 그대로 넘길 수 있는데, 같은 키가 여러 번 있으면 나중에 오는 것이 버려지고 처음 값이 남습니다. 덮어쓰기를 원한다면 insert_or_assign을 반복해야 합니다.

C++ code

// pair 목록을 맵으로 변환
#include <iostream>
#include <set>
#include <string>

int main() {
    std::set<std::string> tags = {"cpp", "stl", "cpp", "rust"};
    std::cout << tags.size() << "\n";
}
알아두면 좋은 점

이미 키 순으로 정렬된 자료라면 insert에 힌트 반복자를 함께 넘겨 삽입 위치를 찾는 과정을 건너뛰게 할 수 있습니다.

자주 하는 실수

map의 insert는 키가 이미 있으면 아무 일도 하지 않으므로, 반환된 쌍의 second를 확인하지 않으면 값이 갱신되지 않은 것을 놓칩니다.

19reverse 복사

이 코드는 결과를 원본 자리에 덮어써 원래 값을 남기지 않습니다. 순서를 뒤집되 원본을 지키고 싶다면 std::reverse 대신 std::reverse_copy로 다른 컨테이너에 써야 합니다. 복사를 새로 만들지 않고 역순 순회만 필요하다면 rbegin()과 rend()를 쓰는 편이 할당 없이 끝납니다.

C++ code

// reverse 복사
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> items = {1, 2, 3, 19};
    std::transform(items.begin(), items.end(), items.begin(), [](int value) { return value * 2; });
    std::cout << items[0] << "\n";
}
알아두면 좋은 점

std::reverse_copy는 출력 범위가 입력과 겹치면 결과가 보장되지 않으므로, 뒤집은 결과는 반드시 별개의 저장소에 써야 합니다.

자주 하는 실수

역방향 반복자를 base()로 되돌리면 가리키는 위치가 한 칸 어긋나므로, 그대로 erase에 넘기면 의도와 다른 원소가 지워집니다.

20partition 나누기

변환은 모든 원소를 같은 방식으로 다루므로 조건에 따라 자리를 가르지는 않습니다. 기준을 만족하는 것을 앞쪽으로 모으려면 std::partition이 그 경계 반복자를 돌려줍니다. 다만 std::partition은 그룹 안의 상대 순서를 보장하지 않으며, 순서를 지키려면 추가 메모리를 쓸 수 있는 std::stable_partition을 골라야 합니다.

C++ code

// partition 나누기
#include <iostream>
#include <set>
#include <string>

int main() {
    std::set<std::string> tags = {"cpp", "stl", "cpp", "rust"};
    std::cout << tags.size() << "\n";
}
알아두면 좋은 점

std::partition은 원소를 서로 맞바꾸며 진행해 추가 메모리 없이 선형 시간에 끝나고, 반환된 반복자가 곧 두 그룹의 경계가 됩니다.

자주 하는 실수

분할 뒤 앞쪽 그룹이 원래 순서를 지킨다고 가정하면 안 되며, 술어를 만족하는 원소들끼리도 자리가 완전히 뒤바뀔 수 있습니다.

정리하며

  • std::remove는 지우지 않습니다. erase와 짝지어 쓰거나 C++20의 erase_if를 사용합니다
  • vector 재할당은 기존 반복자와 포인터를 모두 무효화하므로 순회 중 추가를 피합니다
  • unordered_map의 operator[]는 없는 키를 삽입하니 조회에는 find나 at을 씁니다
  • 그룹핑과 빈도표는 맵에 누적하는 동일 패턴이므로 한 번 익히면 재사용됩니다

더 깊이 들어가고 싶다면 C++ 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.