해시(hash)
해시는 해시 함수를 사용해서 변환한 값을 인덱스로 삼아 키와 값을 저장해서 빠른 데이터 탐색을 제공하는 자료구조
- 보통은 인덱스를 활용하지만 해시는 key를 활용해 데이터 탐색을 빠르게 함
- 키와 값를 일대일 대응해 저장함
해시의 특징
1️⃣ 단방향
키를 통해 값을 찾을 수 있지만 값을 통해 키를 찾을 수는 없음
2️⃣ 찾고자 하는 값을 $O(1)$에서 바로 찾을 수 있음
키 자체가 해시 함수에 의해 값이 있는 인덱스가 되므로 값을 찾기 위한 탐색 과정이 필요 없음
3️⃣ 값을 인덱스로 활용하려면 적절한 변환 과정이 필요
❓ 해시를 사용하지 않으면…
- 전체 데이터를 확인해야하므로 위에서부터 순차적으로 이름을 확인하고
이름4
가 있는 위치 3을 반환- 해당 위치의 전화번호를 읽음
❗ 해시를 사용한다면…
- 순차 탐색할 필요 없이 해시 함수를 활용해 특정 값이 있는 위치를 바로 찾을 수 있음
- 해시 테이블(hash table) : 키와 대응한 값이 저장되어 있는 공간
- 버킷(bucket) : 해시 테이블의 각 데이터
해시의 활용
단방향으로만 검색할 수 있는 대신 빠르게 원하는 값을 검색 가능, 데이터를 저장하거나 보안이 필요할 때 활용됨, 코딩 테스트에서는 데이터를 탐색하는 횟수가 많을 경우 해시를 고려하면 좋음
- 비밀번호 관리
- 데이터베이스 인덱싱
- 블록체인
해시 함수
해시 함수를 구현할 때 고려할 내용
1️⃣ 해시 함수가 변환한 값은 해시 테이블 크기를 넘으면 ❌
- 해시 함수가 변환한 값은 인덱스로 활용되기 때문에 해시 테이블의 크기를 넘으면 안됨
- 해시 테이블 크기가 $N$이라면 인덱스는 $0$ ~ $N-1$ 사이의 값을 내야 함
2️⃣ 해시 함수가 변환한 값의 충돌은 최대한 적게
- 충돌 : 서로 다른 두 키에 대해 해싱 함수를 적용한 결과가 동일한 것
- 해시 함수가 계속해서 같은 위치에 데이터를 저장하면 문제가 발생하므로 이에 따른 다양한 전략이 있음
자주 사용하는 해시 함수
1️⃣ 나눗셈법(division method)
$$h(x)=x\text{ mod } k \\x : \text{키, } k\text{ : 소수 }$$
- $k$ 가 소수인 이유 : 소수가 아니면 약수에 의해 계속해서 충돌이 발생하기 때문
- 테이블 크기 : $k$ ( $0$ ~ $(k-1)$ )
- 단점 : 크기가 $k$ 이므로 상황에 따라 많은 데이터를 저장해야 한다면 큰 소수 필요할 수 있음
2️⃣ 곱셈법(multiplication method)
$$h(x)=(((x*A)\text{ mod }1)*m)\\x \text{ : 키}\\ A \text{ : 황금비(약 1.618033)}\\m\text{ : 최대 버킷의 개수 }$$
- 키에 황금비를 곱함
- 구한 값에서 모듈러 1을 해서 정수부분을 버리고 소수부분만 가지고 감
- 2번 값에 $m$ 을 곱하면 테이블의 인덱스 ( $0$ ~ $(m-1)$ ) 에 매치할 수 있음
※ 황금비를 사용하므로 나눗셈법 처럼 소수가 필요없음, 즉 테이블 크기가 커져도 추가작업 필요 없음
3️⃣문자열 해싱1
$$hash(s)=(s[0]+s[1]*p+s[2]*p^2 + ... +s[n-1]*p^{n-1} )\text{ mod } m \\ s\text{ : 문자열}\\m\text{ : 해시 테이블 최대 크기 }\\ p\text{ : 31(메르센 소수)}$$
※ 메르센 소수 : 일반적으로 $2^{N-1}$ 으로 표시할 수 있는 숫자 중 소수인 수, 해시에서 충돌을 줄이는데 효과적
- 나눗셈, 곱셈법은 키의 자료형이 숫자인 것에 반해 키의 자료형이 문자열일떄도 사용 가능한 문자열 해싱
- 문자열의 문자를 숫자로 변환하고 이 수들을 다항식의 값으로 변환해서 해싱함
- polynomial rolling method 사용
- 단점 : “apple” 이라는 간단한 문자열을 해싱했는데도 결과값은 4,990,970 으로 굉장히 크므로 오버플로우가 발생할 여지가 있음
4️⃣ 문자열 해싱2
$$(a+b)\%c=(a\%c+b\%c)\%c$$
- 좌변 처럼 덧셈을 전부한 다음 모듈러 연산을 하는 대신 우변처럼 중간 중간 모듈러 연산하면 오버플로우 최대한 방지 가능
- 이를 활용한 문자열 해싱 공식은 아래와 같음
$$hash(s)=(s[0]\%m+s[1]*p\%m+s[2]*p^2\%m + ... +s[n-1]*p^{n-1}\%m )\%m$$
충돌 처리
체이닝으로 처리
충돌이 발생하면 해당 버킷에 링크드리스트로 같은 해시값을 가지는 데이터를 연결함
※ LinkedList : 데이터 요소들을 연결하여 구성한 선형 데이터 구조
- 장점 : 간단함
- 단점① : 해시 테이블 공간 활용성 떨어짐
- 충돌이 많아지면 Linked List 길이가 길어지고 다른 해시 테이블의 공간은 덜 사용하게 됨
- 단점② : 검색 성능이 떨어짐
- Linked List 로 연결한 값을 찾으려면 Linked List의 맨 앞 데이터부터 검색해야함
- $N$ 개의 키가 있고 모든 키가 충돌해 체이닝 되었다면 검색 시간 복잡도는 $O(N)$
개방 주소법으로 처리
개방주소법(open addressing)은 빈 버킷을 찾아 충돌 값을 삽입함
- 장점 : 해시 테이블을 최대한 활용하므로 체이닝보다 메모리를 더 효율적으로 사용함
1️⃣ 선형 탐사 방식(Linear Probing)
충돌이 발생하면 다른 빈 버킷을 찾을 때 까지 일정한 간격으로 이동함(보통 간격은 1)
- 테이블의 범위를 넘지 않기 위해 모듈러 연산을 적용
$$h(k,i)=(h(k)+i)\text{ mod }m\\m\text{ : 수용할 수 있는 최대 버킷 }$$
- 단점 : 충돌 발생 시 1 칸씩 이동하며 해시 테이블 빈 곳에 값을 넣으면 해시 충돌이 발생한 값끼리 모이는 영역이 발행 → 클러스터 형성 ⇒ 군집이 생성되면 해시값이 겹칠 학률이 더 올라감
※ 이를 해결하기 위해 제곱수만큼 이동하며 탐사하는 방법도 있음
2️⃣ 이중 해싱 방식
해시 함수를 2개 사용하거나 때로는 $N$개로 늘려서 하는 방법
- 두 번째 해시함수($h_2$) : 첫 번째 해시 함수($h_1$)로 충돌이 발생하면 해당 위치를 기준으로 어떻게 위치를 정할 지 결정
- 클러스터를 줄이기 위해 $m$ 을 제곱수로 하거나 소수로 함
$$h(k,i)=(h_1(k)+i*h_2(k))\text{ mod }m\\m\text{ : 수용할 수 있는 최대 버킷 }$$
해시맵
Java에는 HashTable 클래스와 HashMap 클래스가 있지만 HashTable은 자바의 초기버전과 호환성을 위해 남겨두었을 뿐 최근에는 잘 사용되지 않음
해시맵의 ADT
구분 | 정의 | 설명 |
---|---|---|
연산 | ValueType put(KeyType key, ValueType value) |
key: value로 데이터 저장/동일한 key가 이미 있었다면 그 key의 value값을 반환 |
ValueType get(KeyType key) |
key에 대한 value값을 반환 | |
ValueType getOrDefault(KeyType key, ValueType defaultValue) |
key에 해당하는 value가 없으면 defaultValue 값으로 저장 | |
ValueType remove(KeyType key) |
key에 해당하는 데이터 삭제 | |
ValueType containsKey(KeyType key) |
해당 key가 있다면 true, 없다면 false 반환 | |
void clear() |
해시맵 안의 모든 데이터 삭제 | |
상태 | int isEmpty() |
해시맵 안에 데이터가 없다면 true, 있다면 false 반환 |
int size() |
데이터 개수 반환 |
import java.util.*;
public class hashMap {
public static void main(String[] args) {
// HashMap<KeyType, ValueType>
HashMap<String,Integer> map=new HashMap<>();
// 해시맵에 데이터 추가
map.put("ABC",10);
map.put("BBB",20);
map.put("AAA",30);
map.put("ABC",15);
System.out.println(map.isEmpty()); // false
System.out.println(map.getOrDefault("ABC",40)); // 15
System.out.println(map.containsKey("ABC")); // true
map.remove("ABC"); // 해시맵에서 키가 "ABC"인 데이터 제거
System.out.println(map.size()); // 2
map.clear(); // 해시맵의 모든 데이터 삭제
System.out.println(map.isEmpty()); //true
}
}
'코딩테스트 > 개념정리' 카테고리의 다른 글
[ 자료구조 ] 트리(Tree) (0) | 2025.05.06 |
---|---|
[ 알고리즘 ] 재귀(Recursion) (0) | 2025.04.25 |
[ 자료구조 ] Queue (0) | 2025.04.06 |
[ 자료구조 ] 배열 (0) | 2025.01.13 |
[ 알고리즘 ] Dijkstra Algorithm (0) | 2025.01.08 |