시험공부

(2025-2) Introduction to programming(2) 8. The C++ Libraries II

norepinephrine 2025. 12. 18. 14:53

주제: 연관 컨테이너(ordered/unordered), 정렬/비교·해시·동등성 규칙, 삽입/탐색/삭제 API, pair/emplace, 버킷/로드팩터, 이터레이터 제약, 커스텀 비교자·해시, 문자열/스트림 유틸(문자열 스트림, 포맷팅 기초)


🎯 학습 목표 (Exam Scope)

  • [ ] 정렬 연관 컨테이너: map/multimap/set/multiset의 키 정렬, 유일성, 이터레이터 성격
  • [ ] 비정렬(해시) 연관 컨테이너: unordered_map/unordered_set 계열의 버킷/로드팩터/재해시
  • [ ] 공통 연산: insert/emplace, find/count, lower_bound/upper_bound/equal_range, erase
  • [ ] pair/tuple 기본, map의 operator[] 의미(생성 주의), 범위 기반 삭제 패턴
  • [ ] 커스텀 비교자(정렬 컨테이너)와 커스텀 해시/동등성(해시 컨테이너) 작성
  • [ ] 이터레이터 제약: 키 수정 금지, 탐색/순회 복잡도 차이
  • [ ] 문자열 스트림 istringstream/ostringstream, 포맷팅 기초(채움/너비/정밀도)

0) 큰 그림 한 장

정렬 연관 컨테이너 (트리 기반, 기본: 오름차순 < 비교)
  - set / multiset (키만 저장)
  - map / multimap  (키-값 쌍 저장)

비정렬 연관 컨테이너 (해시 기반, == / hash 필요)
  - unordered_set / unordered_multiset
  - unordered_map / unordered_multimap

  • 정렬 연관: 키가 정렬되어 저장, 탐색 O(log N), 순회가 키 순서
  • 비정렬 연관: 순서 없음, 평균 탐색 O(1), 버킷/로드팩터 관리 필요

1) 정렬 연관 컨테이너 (트리 기반)

1-1. 핵심 특성

컨테이너 키 유일성 저장 값 정렬 기준 이터레이터

set<T> 유일 T std::less<T>(기본 <) 양방향
multiset<T> 중복 허용 T same 양방향
map<K,V> 키 유일 pair<const K, V> same 양방향
multimap<K,V> 중복 허용 pair<const K, V> same 양방향
  • map의 원소 타입은 pair<const K, V>키는 수정 불가(정렬 불변성 유지).
  • 기본 정렬자는 std::less<K>(즉, operator< 기반). 커스텀 비교자 가능(아래 §4-1).

1-2. 기본 연산

#include <map>
#include <set>
#include <string>
using namespace std;

map<string, int> freq;
freq["apple"]++;                 // 없으면 ( "apple", 0 ) 생성 후 증가
freq.insert({"banana", 3});      // 삽입 (이미 있으면 무시)
freq.emplace("cherry", 5);       // 생성+삽입(중간 객체 최소화)

auto it = freq.find("banana");   // O(log N), 없으면 end()
size_t c = freq.count("apple");  // set: 0/1, multiset/map: 0/1, multi*는 0~여러 개
freq.erase("cherry");            // 키로 일괄 삭제

// 중복 키 범위(멀티 계열에서 유용)
auto [first, last] = freq.equal_range("apple"); // [first,last) 구간

1-3. operator[] 주의 (map 한정)

int &v = freq["new_key"];  // 없으면 (new_key, 0) '생성' 후 참조 반환
// 읽기만 하려면 find 사용이 안전

  • 읽기 전용 조회는 find로! operator[]는 부수효과(생성) 가 있음.

2) 비정렬(해시) 연관 컨테이너

2-1. 핵심 특성

컨테이너 키 유일성 평균 탐색 필요 조건

unordered_set<T> 유일 O(1) hash<T>, ==
unordered_multiset<T> 중복 O(1) same
unordered_map<K,V> 유일 O(1) hash<K>, ==
unordered_multimap<K,V> 중복 O(1) same
  • 순서 보장 없음(버킷 순서).
  • 최악 O(N) 가능(충돌 심하면), 평균 O(1) 목표.

2-2. 버킷·로드팩터·재해시

#include <unordered_map>
using namespace std;

unordered_map<string,int> ufreq;
ufreq.reserve(10000);                 // 버킷 수 미리 확보(재해시 감소)
double lf = ufreq.load_factor();      // 평균 버킷당 원소 수
ufreq.max_load_factor(1.0);           // 목표 로드팩터 조정
ufreq.rehash(16384);                  // 버킷 수 직접 설정(재해시)

  • 로드팩터(load factor) = size / bucket_count. 커지면 충돌↑ → rehash/reserve로 관리.

2-3. 해시/동등성 커스텀

#include <unordered_set>
#include <string>
using namespace std;

struct S { string first, last; };

// 동등성: 같은 사람?
struct Eq {
    bool operator()(S const& a, S const& b) const {
        return a.first==b.first && a.last==b.last;
    }
};

// 해시: 두 문자열 해시를 섞기
struct Hash {
    size_t operator()(S const& s) const {
        return std::hash<string>{}(s.first) ^ (std::hash<string>{}(s.last)<<1);
    }
};

unordered_set<S, Hash, Eq> people;

  • 규칙: 같다고 판단(Eq)되는 두 키는 반드시 같은 해시값(Hash)이 나오도록!

3) 공통 패턴: 삽입/탐색/삭제

3-1. 삽입 결과 다루기 (insert 반환)

set<int> st;
auto [pos, ok] = st.insert(10);  // ok==true면 새로 삽입, false면 기존 원소 가리킴

3-2. 반복 중 삭제(안전 패턴)

map<int,int> mp{{1,10},{2,20},{3,30}};
for (auto it = mp.begin(); it != mp.end(); ) {
    if (it->second == 20) it = mp.erase(it);  // erase가 '다음' 반복자를 반환
    else ++it;
}

3-3. 멀티 컨테이너에서 특정 키 전부 삭제

multimap<string,int> mm;
/* ... */
auto range = mm.equal_range("k");
mm.erase(range.first, range.second); // 그 키의 모든 항목 삭제


4) 커스텀 정렬자 / 해시자

4-1. 정렬자(ordered 계열)

struct CaseInsensitiveLess {
    bool operator()(const string& a, const string& b) const {
        auto lower = [](unsigned char c){ return std::tolower(c); };
        size_t n = min(a.size(), b.size());
        for (size_t i=0; i<n; ++i) {
            char ca = lower(a[i]), cb = lower(b[i]);
            if (ca != cb) return ca < cb;
        }
        return a.size() < b.size();
    }
};

map<string,int, CaseInsensitiveLess> dict; // 대소문자 무시 정렬

  • 불변성: 비교자는 엄밀한 약순서(strict weak ordering) 를 만족해야 함(대칭/추이 등).

4-2. 해시자(해시 계열) — 위 §2-3 예시 참고

  • hash<K>와 ==는 동치성을 표현. Eq(a,b)==true ⇒ Hash(a)==Hash(b) 보장 필요.

5) pair/tuple & 구조적 바인딩

#include <utility>
#include <tuple>
using namespace std;

pair<string,int> p{"apple", 3};
auto [name, cnt] = p;     // C++17 구조적 바인딩
// make_pair, tie, tuple도 유사하게 사용 가능

  • map 순회에서 자주 쓰는 패턴
for (auto const& [k,v] : freq) {
    // k, v 바로 사용
}


6) 이터레이터 제약(연관 컨테이너 전용 주의)

  • map/set 등의 이터레이터는 양방향. (list처럼 - 가능, +n 불가)
  • map 원소 타입: pair<const K, V> → 키는 수정 불가. 값(V)만 수정 가능.
for (auto& [k, v] : freq) v += 1;    // OK
// k = "new";                        // ❌ 키 변경 금지


7) 문자열 스트림 & 포맷팅(간단)

7-1. 문자열 스트림

#include <sstream>
#include <string>
#include <vector>
using namespace std;

string line = "10 20 30";
istringstream iss(line);  // 문자열을 입력 스트림처럼
int x; vector<int> v;
while (iss >> x) v.push_back(x);

ostringstream oss;
oss << "sum=" << 60;
string msg = oss.str();   // "sum=60"

7-2. 포맷팅 기본

#include <iomanip>
double d = 3.1415926;
cout << fixed << setprecision(2) << setw(8) << setfill('_') << d << "\\n";
// 출력 예: "__3.14"


8) 실전 스니펫 모음

8-1. 단어 빈도 (정렬 버전)

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

int main() {
    map<string,int> freq;
    for (string w; cin >> w; ) ++freq[w]; // operator[]가 자동 생성+증가
    for (auto const& [w,c] : freq) cout << w << " " << c << "\\n";
}

8-2. 단어 빈도 (해시 버전, 성능형)

#include <unordered_map>
#include <string>
#include <iostream>
using namespace std;

int main() {
    unordered_map<string,int> freq;
    freq.reserve(1<<16);                  // 성능 최적화
    for (string w; cin >> w; ) ++freq[w];
    // 순서는 비결정적
}

8-3. 접두사/접미사 집계: 멀티맵 활용

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

multimap<string,string> groups;
groups.emplace("pre", "preview");
groups.emplace("pre", "prefix");
groups.emplace("post","postfix");

auto [first,last] = groups.equal_range("pre");
for (auto it=first; it!=last; ++it) /* it->second 사용 */;

8-4. 사용자 타입 해시/동등성

#include <unordered_set>
#include <string>
using namespace std;

struct User { string id; int lvl; };
struct Eq { bool operator()(User const&a, User const&b) const { return a.id==b.id; } };
struct Hash { size_t operator()(User const&u) const { return hash<string>{}(u.id); } };

unordered_set<User,Hash,Eq> users;


9) 자주 틀리는 포인트 (체크리스트)

  • [ ] map::operator[]는 없던 키를 생성한다 → 조회만이면 find 사용
  • [ ] set/map의 키는 수정 불가(원소 타입이 pair<const K, V>)
  • [ ] unordered_*는 평균 O(1)이지만 로드팩터 관리가 중요(reserve/rehash)
  • [ ] 커스텀 해시/동등성: 동치 ⇒ 같은 해시 규칙 지키기
  • [ ] equal_range는 멀티 계열에서 해당 키 구간을 한 번에 얻는 도구
  • [ ] 연관 컨테이너 이터레이터는 임의 접근 아님(정렬 컨테이너: 양방향)
  • [ ] erase 중 순회는 it = c.erase(it) 패턴으로 안전하게

🔎 미니 퀴즈 (해설 간단)

  1. map<string,int> m;에서 m["x"]와 m.at("x") 차이?

[]: 없으면 생성(0으로 초기화). at: 없으면 예외.

  1. unordered_map에서 성능이 급락한다. 무엇을 점검?

load_factor / bucket_count 확인, reserve/rehash로 조정.

  1. multimap에서 "k" 키를 전부 순회하려면?
auto [first,last] = mm.equal_range("k");
for (auto it=first; it!=last; ++it) /* ... */;

  1. 아래 선언의 의미는?
map<string,int,MyLess> m;

MyLess 비교자를 사용해 사용자 정의 정렬 기준으로 키를 정렬.

  1. 왜 map의 키는 수정할 수 없나?

정렬 불변성 유지. 원소 타입이 pair<const K,V>이므로 키는 const.


✅ 마무리 요약

  • 정렬 연관: 순서·범위질의(lower_bound/equal_range) 강점, O(log N)
  • 비정렬 연관: 평균 O(1) 탐색, 버킷/로드팩터 관리 필수
  • 공통 API: insert/emplace/find/count/erase, 멀티 계열은 equal_range
  • 커스텀 비교/해시로 도메인 규칙을 녹여라
  • 문자열 스트림과 포맷팅은 입출력 전처리/후처리에 유용