Quick Syntax
| 구조 | 저장 | 평균 조회 | 대표 용도 |
|---|---|---|---|
Dictionary<TKey,TValue> | 키와 값 | O(1) | 빈도, 매핑, 인덱스 찾기 |
HashSet<T> | 키만 | O(1) | 중복 제거, 포함 검사 |
| 배열 카운팅 | 정수 인덱스 | O(1) | 값 범위가 작을 때 |
string[] words = { "api", "cache", "api" };
var count = new Dictionary<string, int>();
foreach (string word in words)
{
count[word] = count.GetValueOrDefault(word) + 1;
}키 비교는 Equals가 같은 두 값이 반드시 같은 hash code를 내야 한다는 계약을 따릅니다. 사용자 정의 key에는 IEquatable<T>와 GetHashCode를 함께 구현하거나 IEqualityComparer<T>를 넘깁니다. 저장한 뒤 비교·hash에 쓰는 필드를 바꾸면 같은 key를 다시 찾지 못할 수 있으므로 key는 사실상 불변이어야 합니다.
키 존재와 값을 함께 다루면 Dictionary<TKey, TValue>, 중복 없는 존재만 보면 HashSet<T>을 고릅니다. 평균 O(1)은 올바른 equality와 hash code 계약을 전제로 합니다.
구조
해시 테이블은 키를 해시 함수에 넣어 정수값으로 바꾸고, 그 값을 내부 버킷 위치로 매핑합니다. 그래서 정렬 없이도 평균적으로 빠른 조회가 가능합니다.
key -> hash code -> bucket index -> stored entry서로 다른 키가 같은 버킷에 들어가는 상황을 충돌이라고 합니다. 충돌이 많아지면 한 버킷 안에서 추가 탐색이 필요해지고, 평균 O(1)에 가까운 장점이 줄어듭니다.
Load factor는 원소 수와 bucket 수의 비율입니다. 구현이 임계치를 넘으면 bucket 배열을 키우고 기존 entry를 다시 배치하는 rehash를 수행할 수 있어 개별 삽입은 비싸질 수 있습니다. C++ std::unordered_map의 사용자 정의 key는 equality가 같은 값에 같은 hash를 반환해야 하며, 저장 중 hash에 쓰는 필드를 바꾸지 않습니다.
bucketIndex = hash(key) % bucketCount
bucket[bucketIndex] -> entry -> entry // separate chaining 예시Separate chaining은 같은 bucket의 entry를 연결하고, open addressing은 빈 slot을 probe합니다. 구현이 무엇인지에 따라 삭제, load factor와 iterator invalidation 계약이 달라집니다. C++에서 원소 수를 예상하면 reserve(n)으로 rehash 횟수를 줄일 수 있지만 reference·iterator 무효화 규칙은 container 문서를 확인합니다.
Dictionary와 HashSet
Dictionary<TKey,TValue>는 키로 값을 찾을 때 씁니다. HashSet<T>는 값이 존재하는지만 중요할 때 씁니다.
var indexByName = new Dictionary<string, int>();
indexByName["mina"] = 3;
var seen = new HashSet<int>();
seen.Add(10);
if (seen.Contains(10))
{
// 이미 본 값
}없는 키를 읽을 때 예외를 피하려면 TryGetValue가 키 존재와 값을 한 번에 확인합니다. 이미 있을 때만 거부하고 싶으면 TryAdd, 기본값이 자연스러우면 GetValueOrDefault를 고릅니다.
if (indexByName.TryGetValue("mina", out int index))
{
Console.WriteLine(index);
}
bool added = indexByName.TryAdd("jisu", 4);
var caseInsensitive = new HashSet<string>(StringComparer.OrdinalIgnoreCase);StringComparer.OrdinalIgnoreCase처럼 비교 규칙을 생성 시점에 넘기면, 추가·조회·삭제가 같은 equality와 hash 규칙을 공유합니다. 중간에 비교 기준을 바꾸는 방법은 없습니다.
선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 값의 등장 횟수 세기 | Dictionary<T, int> |
| 중복 여부 확인 | HashSet<T> |
| 키로 다른 값 찾기 | Dictionary<TKey,TValue> |
| 값 범위가 작고 정수 | 배열 카운팅 |
| 정렬 순서가 필요 | 정렬된 구조 또는 정렬 후 처리 |
해시는 “빠른 포함 검사”가 필요한 O(n²) 풀이를 O(n)에 가깝게 줄일 때 자주 등장합니다.
var seen = new HashSet<int>();
foreach (int x in nums)
{
int need = target - x;
if (seen.Contains(need)) return true;
seen.Add(x);
}주의할 점
해시 테이블의 O(1)은 평균 기대값입니다. 키의 해시 품질이 나쁘거나 충돌이 많으면 성능이 떨어질 수 있습니다.
또 Dictionary는 없는 키를 인덱서로 읽으면 예외가 납니다. TryGetValue, ContainsKey, GetValueOrDefault 중 하나로 없는 키 처리를 명확히 해야 합니다.
foreach로 순회하는 동안 같은 Dictionary나 HashSet을 수정하면 열거가 무효화됩니다. 삭제 대상은 따로 모으거나 복사본을 순회하세요.
참고 링크
3 sources