Company
교육 철학

unordered_map, counting sort(계수 정렬)

계수정렬 (Counting Sort) & 해시테이블 (Hash Table)

계수정렬 (Counting Sort)

핵심 개념

Counting sort는 정렬 알고리즘중 하나로, 키 값의 범위가 제한된 정수 일때 매우 효율적인 방법. 다른 비교기반 정렬 알고리즘과 달리, 비교를 하지 않고 숫자의 발생 횟수를 카운트하여 정렬을 수행한다.
시간 복잡도가 O(n+k)이다. 데이터크기와 데이터 값의 범위에 비례한다.

카운팅소트의 동작원리

1. 입력데이터와 범위를 미리 알아두어야 한다. 예를 들어 0에서 100사이의 정수만을 다룬다면, 해당범위에 맞춰 알고리즘 동작

2. 각 데이터가 몇번씩 등장했는지 기록하는 카운트 배열을 만든다.

3. 카운트 배열을 통해 해당 숫자보다 작은 숫자가 몇개 있는지 계산한다.

4. 원래 배열의 숫자를 하나씩 꺼내면서 정렬된 위치에 배치한다.

예제: [5, 2, 2, 1, 2] 정렬

1단계: 정렬할 값들을 count 배열에 저장한다.

원본 배열: [5, 2, 2, 1, 2] 카운트 배열 구성: 숫자 | 0 | 1 | 2 | 3 | 4 | 5 | 개수 | 0 | 1 | 3 | 0 | 0 | 1 | 각 숫자의 등장 횟수를 센다: - 1이 1번 등장 - 2가 3번 등장 - 5가 1번 등장
Plain Text
복사

2단계: 누적합을 구해준다.

누적합 계산 과정: index 0: 0 index 1: 0 + 1 = 1 index 2: 1 + 3 = 4 index 3: 4 + 0 = 4 index 4: 4 + 0 = 4 index 5: 4 + 1 = 5 결과 배열: [0, 1, 4, 4, 4, 5]
Plain Text
복사

누적합을 구하는 이유

신기하게도 정렬할 값들이 result 배열에 들어 갈 수 있는 LastIndex가 결정된다.
예시 분석: - 숫자 5는 result[5-1]에 들어가면 된다. - 숫자 2는 result[4-1]에 들어가면 된다. - 숫자 1은 result[1-1]에 들어가면 된다. (index에 -1을 해주면 result[0]부터 채울 수 있다.) 최종 결과: [1, 2, 2, 2, 5]
Plain Text
복사

3단계: 누적합을 이용하여 정렬 결과 저장

원본 배열을 뒤에서부터 처리하여 안정 정렬 보장: 3단계 과정 (5단계별 세부 처리): 1. 숫자 2 처리: count[2]=4 → result[3]=2, count[2]=3 2. 숫자 1 처리: count[1]=1 → result[0]=1, count[1]=0 3. 숫자 2 처리: count[2]=3 → result[2]=2, count[2]=2 4. 숫자 2 처리: count[2]=2 → result[1]=2, count[2]=1 5. 숫자 5 처리: count[5]=5 → result[4]=5, count[5]=4 정렬된 값을 하나씩 담색하면서, 제 위치에다가 값을 저장한다.
Plain Text
복사

계수정렬 완전 구현

#include <iostream> #include <queue> int main() { int arr[5] = { 5, 2, 2, 1, 2 }; int bucket[6] = { }; // 카운트 배열 int result[5] = { }; // 결과 배열 //1. bucket에 arr의 값을 넣어준다. for (size_t i = 0; i < 5; i++) { bucket[arr[i]]++; } //2. bucket의 값을 누적시킨다. for (size_t i = 1; i < 6; i++) { bucket[i] += bucket[i - 1]; } //3. result에 bucket의 값을 넣어준다. for (size_t i = 0; i < 5; i++) { int index = bucket[arr[i]] - 1; result[index] = arr[i]; bucket[arr[i]]--; // 같은 값의 다음 위치를 위해 감소 } // 결과 출력 std::cout << "정렬 결과: "; for (int i = 0; i < 5; i++) { std::cout << result[i] << " "; } std::cout << std::endl; return 0; }
C++
복사

계수정렬의 특징

장점

1.
매우 빠른 속도: O(n+k) 시간복잡도
2.
안정 정렬: 같은 값의 상대적 순서 유지
3.
비교 없음: 값의 비교 연산 불필요

단점

1.
범위 제한: 정수만 가능, 범위가 클 수록 비효율
2.
추가 메모리: O(k) 공간 필요 (k = 값의 범위)
3.
범위 의존적: 값의 범위가 크면 사용 불가

사용 적합한 경우

정수 데이터
값의 범위가 작을 때 (보통 n과 비슷하거나 작을 때)
안정 정렬이 필요한 경우
최고 성능이 필요한 경우

해시테이블 (Hash Table) - unordered_map

핵심 개념

C++에서 제공하는 해시테이블 기반의 컨테이너입니다. 이 컨테이너는 키값과 데이터값 쌍으로 이루어져있다.
키를 기준으로 인덱스에 접근하기 때문에 데이터를 검색, 삽입할때 빠른속도로 사용 할 수 있다.

해시테이블의 특징

1. 해시 기반: 내부적으로 해시함수를 사용해서 키를 계산하기 때문에, 평균 시간 복잡도가 삽입, 삭제, 검색 전부 O(1)이다.

2. 키의 순서 보장이 되지 않는다: 삽입되는 순서가 유지되지 않는다. 내부적으로 해시테이블에 저장되기때 문에, 순서가 무작위이다.

3. 중복되는 키값을 허용 않는다: 각 키는 고유해야 하며, 중복된 값을 사용하면 그냥 덮어 씌어진다

4. 시간복잡도는 O(1)이다: 그치만 해시 충돌이 많이질 경우 O(n)까지도 늘어날수도 있다.

unordered_map 기본 사용법

#include <iostream> #include <unordered_map> int main() { std::unordered_map<std::string, int> age_map; // 삽입 age_map["Alice"] = 25; //age_map.insert(std::make_pair("Bob", 30)); age_map["Bob"] = 30; age_map["Charlie"] = 22; // 값 접근 std::cout << "Alice's age: " << age_map["Alice"] << std::endl; // 존재 여부 확인 if (age_map.find("Bob") != age_map.end()) { std::cout << "Bob is in the map." << std::endl; } // 삭제 age_map.erase("Charlie"); // 전체 순회 for (const auto& pair : age_map) { std::cout << pair.first << ": " << pair.second << std::endl; } return 0; }
C++
복사

주요 함수들

삽입 연산

// 방법 1: [] 연산자 map["key"] = value; // 방법 2: insert 함수 map.insert(std::make_pair("key", value)); map.insert({"key", value}); // C++11 이후
C++
복사

조회 연산

// 방법 1: [] 연산자 (없으면 기본값으로 생성) int value = map["key"]; // 방법 2: at() 함수 (없으면 예외 발생) int value = map.at("key"); // 방법 3: find() 함수 (가장 안전) auto it = map.find("key"); if (it != map.end()) { int value = it->second; }
C++
복사

삭제 연산

// 키로 삭제 map.erase("key"); // 반복자로 삭제 auto it = map.find("key"); if (it != map.end()) { map.erase(it); } // 전체 삭제 map.clear();
C++
복사

기타 유용한 함수들

// 크기 확인 size_t size = map.size(); // 비어있는지 확인 bool empty = map.empty(); // 키 존재 여부 (C++20) bool exists = map.contains("key"); // 개수 세기 (0 또는 1) size_t count = map.count("key");
C++
복사

해시테이블 vs 다른 자료구조

구분
unordered_map
map
vector
평균 시간복잡도
O(1)
O(log n)
O(n)
최악 시간복잡도
O(n)
O(log n)
O(n)
순서 보장
(정렬됨)
(삽입순)
메모리 사용
높음
보통
낮음
사용 적합성
빠른 검색
정렬된 데이터
순차 접근

실전 활용 예제

1. 문자 빈도 계산

std::unordered_map<char, int> charCount; std::string text = "hello world"; for (char c : text) { charCount[c]++; // 없으면 0으로 초기화 후 증가 } // 결과: h(1), e(1), l(3), o(2), ' '(1), w(1), r(1), d(1)
C++
복사

2. 두 수의 합 문제

std::vector<int> twoSum(std::vector<int>& nums, int target) { std::unordered_map<int, int> numMap; for (int i = 0; i < nums.size(); i++) { int complement = target - nums[i]; if (numMap.find(complement) != numMap.end()) { return {numMap[complement], i}; } numMap[nums[i]] = i; } return {}; }
C++
복사

주의사항

해시 충돌

// 해시 충돌이 많이 발생하면 성능 저하 // 좋은 해시 함수를 사용하는 것이 중요 // 커스텀 해시 함수 예제 struct StringHash { size_t operator()(const std::string& s) const { return std::hash<std::string>{}(s); } };
C++
복사

메모리 사용량

// 해시테이블은 추가 메모리를 많이 사용 // 로드 팩터(load factor)에 따라 메모리 사용량 결정 // 기본적으로 75% 로드 팩터에서 재해싱
C++
복사

핵심 정리

계수정렬

1.
O(n+k) 시간복잡도로 매우 빠름
2.
정수 범위가 제한적일 때만 사용 가능
3.
안정 정렬이며 비교 연산 불필요
4.
추가 메모리 O(k) 필요

해시테이블 (unordered_map)

1.
평균 O(1) 시간복잡도로 빠른 검색/삽입/삭제
2.
키의 순서 보장 안됨
3.
해시 충돌 시 성능 저하 가능
4.
빈도 계산, 캐시, 매핑 등에 유용
unordered_map은 순서를 신경 쓰지 않고 빨르게 키 기반으로 데이터를 조회하고자 할 때 매우 유용한 컨테이너입니다.
이 코드는 간단한 캐시(Cache) 클래스입니다!

SimpleCache 클래스 상세 분석

캐시란?

자주 사용하는 데이터를 빠르게 접근할 수 있도록 임시 저장하는 공간
예시: - 웹사이트 방문 기록 저장 - 계산 결과 임시 보관 - 사용자 정보 빠른 조회
Plain Text
복사

클래스 구조 분석

1. Private 멤버

std::unordered_map<std::string, int> cache;
C++
복사
: std::string (예: 사용자 이름, ID 등)
: int (예: 나이, 점수, 카운트 등)
해시테이블 사용 → 빠른 O(1) 접근 속도

2. Public 함수들

put() 함수 - 데이터 저장

void put(const std::string& key, int value) { cache[key] = value; }
C++
복사
역할: 키-값 쌍을 캐시에 저장
동작: cache["이름"] = 25 형태로 저장
특징: 같은 키가 있으면 값을 덮어씀

get() 함수 - 데이터 조회

int get(const std::string& key) { auto it = cache.find(key); return (it != cache.end()) ? it->second : -1; }
C++
복사
역할: 키에 해당하는 값 반환
안전한 조회: 키가 없으면 -1 반환 (크래시 방지)
삼항 연산자: 조건 ? 참일때값 : 거짓일때값

exists() 함수 - 존재 여부 확인

bool exists(const std::string& key) { return cache.find(key) != cache.end(); }
C++
복사
역할: 키가 캐시에 있는지 확인
반환: true(있음) 또는 false(없음)

실사용 예제

#include <iostream> #include <unordered_map> #include <string> class SimpleCache { private: std::unordered_map<std::string, int> cache; public: void put(const std::string& key, int value) { cache[key] = value; std::cout << "저장: " << key << " = " << value << std::endl; } int get(const std::string& key) { auto it = cache.find(key); if (it != cache.end()) { std::cout << "조회 성공: " << key << " = " << it->second << std::endl; return it->second; } else { std::cout << "조회 실패: " << key << " (없음)" << std::endl; return -1; } } bool exists(const std::string& key) { return cache.find(key) != cache.end(); } void showAll() { std::cout << "\n=== 전체 캐시 내용 ===" << std::endl; for (const auto& pair : cache) { std::cout << pair.first << ": " << pair.second << std::endl; } } }; int main() { SimpleCache myCache; // 데이터 저장 myCache.put("Alice", 25); myCache.put("Bob", 30); myCache.put("Charlie", 22); // 데이터 조회 int age = myCache.get("Alice"); // 25 반환 int unknown = myCache.get("David"); // -1 반환 (없음) // 존재 여부 확인 if (myCache.exists("Bob")) { std::cout << "Bob이 캐시에 있습니다!" << std::endl; } if (!myCache.exists("Eve")) { std::cout << "Eve는 캐시에 없습니다." << std::endl; } // 전체 내용 출력 myCache.showAll(); return 0; }
C++
복사

실행 결과

저장: Alice = 25 저장: Bob = 30 저장: Charlie = 22 조회 성공: Alice = 25 조회 실패: David (없음) Bob이 캐시에 있습니다! Eve는 캐시에 없습니다. === 전체 캐시 내용 === Alice: 25 Bob: 30 Charlie: 22
Plain Text
복사

핵심 포인트

1. 안전한 조회

// ❌ 위험한 방법 (키가 없으면 0으로 생성됨) int value = cache["nonexistent"]; // ✅ 안전한 방법 (키가 없으면 -1 반환) int value = get("nonexistent");
C++
복사

2. find() 함수의 작동 원리

auto it = cache.find("Alice"); if (it != cache.end()) { // 찾았음! it->second가 값 std::cout << it->first << ": " << it->second << std::endl; } else { // 못찾음! std::cout << "키가 없습니다." << std::endl; }
C++
복사

3. 왜 -1을 반환하나?

0은 유효한 값일 수 있음 (나이가 0살인 경우)
1은 일반적으로 "없음"을 나타내는 관례
더 좋은 방법: std::optional<int> 사용 (C++17 이후)

개선된 버전

#include <optional> // C++17 class BetterCache { private: std::unordered_map<std::string, int> cache; public: void put(const std::string& key, int value) { cache[key] = value; } // optional을 사용한 더 안전한 get std::optional<int> get(const std::string& key) { auto it = cache.find(key); if (it != cache.end()) { return it->second; // 값이 있으면 반환 } return std::nullopt; // 없으면 nullopt 반환 } bool exists(const std::string& key) { return cache.find(key) != cache.end(); } }; // 사용법 BetterCache cache; cache.put("Alice", 25); auto result = cache.get("Alice"); if (result.has_value()) { std::cout << "Alice의 나이: " << result.value() << std::endl; } else { std::cout << "Alice를 찾을 수 없습니다." << std::endl; }
C++
복사

실전 활용 사례

1. 웹서버 세션 관리

SimpleCache sessionCache; sessionCache.put("user123", 1); // 로그인됨 sessionCache.put("user456", 0); // 로그아웃됨
C++
복사

2. 계산 결과 캐싱

SimpleCache fibCache; fibCache.put("fib_10", 55); // 피보나치(10) = 55 저장 fibCache.put("fib_15", 610); // 피보나치(15) = 610 저장
C++
복사

3. 게임 플레이어 점수

SimpleCache scoreCache; scoreCache.put("player1", 1500); scoreCache.put("player2", 2300);
C++
복사

요약

SimpleCache는:
해시테이블 기반의 빠른 캐시
put(): 저장, get(): 조회, exists(): 확인
O(1) 평균 성능으로 매우 빠름
간단하지만 실용적인 캐시 구현체
"자주 쓰는 데이터를 빠르게 꺼내 쓸 수 있는 임시 저장소" 라고 생각하면 됩니다!