각 위치(슬롯)마다 주소가 부여되어 있는 저장공간
저장할 버킷이 중복되는 현상
같은 값을 갖는 데이터를 쇠사슬(chain) 모양으로 연결 리스트에서 연결하는 방법
public class ChainHash<K,V> {
/**
* 해시 구성 노드
*/
class Node<K,V> {
private K key; // 키 값
private V value; // 데이터
private Node<K,V> nextNode; // 참조하는 다음 노드
/**
* 생성자
*/
Node(K key, V value, Node<K,V> next) {
this.key = key;
this.value = value;
this.next = next;
}
/**
* 키 값 반환 메서드
*/
K getKey() {
return key;
}
/**
* 데이터 반환 메서드
*/
V getValue() {
return value;
}
/**
* 키의 해시 값 반환 메서드
*/
public int hashCode() {
return key.hashCode();
}
}
private int size; // 해시 테이블 크기
private Node<K,V>[] table; // 해시 테이블
/**
* 생성자
* - 호출 직후 배열 table의 모든 요소는 null을 참조
* - 즉, 모든 버킷이 비어있는 상태가 됨
*/
public ChainHash(int capacity) {
try {
table = new Node[capacity];
this.size = capacity;
} catch(OutOfMemoryError e) {
this.size = 0;
}
}
/**
* 해시 값을 구하는 메서드
*/
public int hashValue(Object key) {
return key.hashCode() % size;
}
/**
* 키 값 key를 갖는 요소 검색 메서드(데이터 반환)
* -
*/
public V search(K key) {
int hash = hashValue(key); // 검색할 데이터의 해시 값
Node<K,V> currentNode = table[hash]; // 선택한 노드
while(currentNode != null) {
if(currentNode.getKey().equals(key)) {
return currentNode.getValue(); // 검색 성공
}
currentNode = currentNode.next; // 다음 노드 가리키기
}
return null; // 검색 실패
}
/**
* 키 값 key, 데이터 value를 갖는 요소 추가 메서드
*/
public int add(K key, V value) {
int hash = hashValue(key); // 추가할 데이터의 해시 값
Node<K,V> currentNode = table[hash]; // 선택한 노드
while(currentNode != null) {
// 이미 등록된 키 값인 경우
if(currentNode.getKey().equals(key)) {
return 1;
}
currentNode = currentNode.next; // 다음 노드 가리키기
}
Node<K,V> temporary = new Node<K,V>(key, value, table[hash];
table[hash] = temporary; // 노드 삽입
return 0;
}
/**
* 키 값 key를 갖는 요소를 삭제하는 메서드
*/
public int remove(K key) {
int hash = hashValue(key); // 삭제할 데이터의 해시 값
Node<K,V> currentNode = table[hash]; // 선택한 노드
Node<K,V> preNode = null; // 바로 앞의 선택 노드
while(currentNode != null) {
// 찾은 경우
if(currentNode.getKey().equals(key)) {
if(preNode == null) {
table[hash] = currentNode.next;
} else {
preNode.next = currentNode.next;
}
return 0;
}
preNode = currentNode;
currentNode = currentNode.next; // 다음 노드 가리키기
}
return 1; // 해당 키 값이 없음
}
}
비어 있는 해시 테이블 생성
capacity
인 배열 table
의 본체를 생성하고 capacity
값을 필드 size
에 복사table
의 모든 요소는 null을 참조(모든 버킷이 비어있는 상태)Node<K,V>
에 대한 참조형이므로, 참조형 배열인 table
의 초기 설정값이 널 참조가 됨키 값이
key
인 요소를 검색하는 메서드
✅ 검색 과정
1. 해시 함수가 키 값을 해시 값으로 변환한다.
2. 해시 값을 인덱스로 하는 버킷을 선택한다.
3. 선택한 버킷의 연결 리스트를 처음부터 순서대로 검색한다.
3-1. 키 값과 같은 값을 찾은 경우 → 검색 성공
예시) 검색하려는 값 : 33, 해시 값 : 7
table[7]이 가리키는 연결 리스트를 하나씩 끌어당기며 검색하다가 해당 값을 찾음
3-2. 끝까지 스캔하여 찾지 못한 경우 → 검색 실패
예시) 검색하려는 값 : 26, 해시 값 : 0
table[0]이 null이므로 검색 실패
키 값이
key
이고 데이터가value
인 요소를 삽입하는 메서드
✅ 과정
1. 해시 함수가 키 값을 해시 값으로 변환한다.
2. 해시 값을 인덱스로 하는 버킷을 선택한다.
3. 선택한 버킷의 연결 리스트를 처음부터 순서대로 검색한다.
3-1. 키 값과 같은 값을 찾은 경우 → 키 값이 이미 등록된 상태(실패)
3-2. 끝까지 스캔하여 찾지 못한 경우 → 리스트의 맨 앞 위치에 노드 삽입
키 값이
key
인 요소를 삭제하는 메서드
✅ 과정
1. 해시 함수가 키 값을 해시 값으로 변환한다.
2. 해시 값을 인덱스로 하는 버킷을 선택한다.
3. 선택한 버킷의 연결 리스트를 처음부터 순서대로 검색한다.
3-1. 키 값과 같은 값을 찾은 경우 → 해당 노드를 리스트에서 삭제
3-2. 끝까지 스캔하여 찾지 못한 경우 → 삭제 실패
충돌이 발생했을 때 재해시(refrashing)를 수행하여 비어 있는 버킷을 찾아내는 방법
public class OpenHash<L,V> {
/**
* 버킷 상태
* - {데이터 저장, 비어있음, 삭제 마침}
*/
enum Status {OCCUPIED, EMPTY, DELETED};
/**
* 버킷 클래스
*/
static class Bucket<K,V> {
private K key; // 키 값
private V value; // 데이터
private Status status; // 상태
/**
* 생성자
*/
Bucket() {
status = Status.EMPTY; // 버킷은 비어있는 상태
}
/**
* 모든 필드에 값을 설정하는 메서드
*/
void set(K key, V value, Status status) {
this.key = key; // 키 값
this.value = value; // 데이터
this.status = status; // 상태
}
/**
* 상태를 설정하는 메서드
*/
void setStatus(Status status) {
this.status = status;
}
/**
* 키 값을 반환하는 메서드
*/
K getKey() {
return key;
}
/**
* 데이터를 반환하는 메서드
*/
v getValue() {
return value;
}
/**
* 키의 해시 값을 반환하는 메서드
*/
public int hashCode() {
return key.hashCode();
}
}
private int size; // 해시 테이블 크기
private Bucket<K,V>[] table; // 해시 테이블
/**
* 생성자
*/
public OpenHash(int size) {
try {
table = new Bucket[size];
for(int index = 0; index < size; index++) {
table[index] = new Bucket<K,V>();
}
this.size = size;
} catch(OutOfMemoryError e) { // 테이블 생성 불가
this.size = 0;
}
}
/**
* 해시 값을 구하는 메서드
*/
public int hashValue(Object key) {
return key.hashCode() % size;
}
/**
* 재해시 값을 구하는 메서드
*/
public int rehashValue(int hash) {
return (hash + 1) % size;
}
/**
* 키 값 key를 갖는 버킷을 검색하는 메서드
*/
private Bucket<K,V> searchNode(K key) {
int hash = hashValue(key); // 검색할 데이터의 해시 값
Bucket<K,V> currentNode = table[hash]; // 선택한 버킷
for(int index = 0;
currentNode.status != Status.EMPTY && index < size;
index++
) {
if(currentNode.status == Status.OCCUPIED && currentNODE.getKey().equals(key)) {
return currentNode;
}
hash = rehashValue(hash); // 재해시
currentNode = table[hash];
}
return null;
}
/**
* 키 값 key를 갖는 요소를 검색하는 메서드(데이터 반환)
*/
public V search(K key) {
Bucket<K,V> currentNode = searchNode(key);
if(currentNode != null) {
return currentNode.getValue();
} else {
return null;
}
}
/**
* 키 값 key, 데이터 value를 갖는 요소를 추가하는 메서드
*/
public int add(K key, V value) {
if (search(key) != null) {
return 1; // 키 값이 이미 등록되어 있음
}
int hash = hashValue(key); // 추가할 데이터의 해시 값
Bucket<K,V> p = table[hash]; // 선택한 버킷
for(int index = 0; index < size; index++) {
if(currentNode == Status.EMPTY || currentNode == Status.DELETED) {
currentNode.set(key, value, Status.OCCUPIED);
return 0;
}
hash = rehashValue(hash); // 재해시
currentNode = table[hash];
}
return 2; // 해시 테이블이 가득 참
}
/**
* 키 값 key를 갖는 요소를 삭제하는 메서드
*/
public int remove(K key) {
Bucket<K,V> currentNode = searchNode(key); // 선택한 버킷
if(currentNode == null) {
return 1; // 등록되지 않은 키 값임
}
currentNode.setStatus(Status.DELETED);
return 0;
}
}
인덱스에 해당하는 버킷의 데이터 a
를 삭제하는 경우, 같은 해시 값을 갖는 다른 데이터 b
를 검색할 때 데이터 b
가 존재하지 않는다고 생각하게 됨(검색 실패)
→ 버킷에 대한 속성 부여 필요
속성 | 설명 | 비고 |
---|---|---|
OCCUPIED | 데이터 저장 | |
EMPTY | 비어 있음 | 같은 해시 값의 데이터가 존재하지 않음 |
DELETED | 삭제 마침 | 같은 해시 값의 데이터가 다른 버킷에 저장되어 있음 |
- 충돌이 전혀 발생하지 않는다는 전제가 필요
- 해시 함수로 인덱스를 찾는 것만으로 검색, 추가, 삭제가 거의 완료
항목 | 복잡도 |
---|---|
삽입 | O(1) |
삭제 | O(1) |
검색 | O(1) |
해시 함수가 모든 키를 동일한 해시 값으로 매핑하여 모든 항목이 하나의 버킷에 저장되는 경우
항목 | 복잡도 |
---|---|
삽입 | O(n) |
삭제 | O(n) |
검색 | O(n) |
항목 | 설명 |
---|---|
해시 테이블 (hash table) | 해시 값이 인덱스가 되도록 원래의 키 값을 저장한 배열 |
해시 함수 (hash function) | 키 값을 가지고 해시 값을 만드는 과정 |
버킷(bucket) | 해시 테이블의 각 요소 |
📖 참고
- 이관용·김진욱(2024). 알고리즘. 한국방송통신대학교출판문화원
- Bohyoh Shibata. (2020). Do it! 자료구조와 함께 배우는 알고리즘 입문 - 자바편(강민 옮김). 이지스퍼블리싱