Map 이란?
Hash 란?
HashMap 이란?

Fig1. HashMap, HashTable 구조 (https://medium.com/depayse/kotlin-data-structure-hashtable-ebb9f949e936 의 이미지 참고)
Hash 함수를 거친 HashMap을 보게 되면, 배열로 이루어져 있다.
배열의 원소에는 또 여러 개의 데이터가 들어있다.
이유는 서로 다른 두 입력 값이 Hash function을 거쳐 나온 HashMap의 주소를 가리키는 곳이 같을 수도 있기 때문이다. 이런 경우를 collision(충돌)이라고 한다.
만약 Hash 함수가 여러 입력 값에 대해 같은 결과를 많이 만든다면, collision이 많아질 수 있고, 이렇게 되면 데이터를 찾는데 더 오랜 시간이 걸릴 수 있다. 따라서 Hash function을 잘 설계하는 것도 HashMap의 효율성에 연관된다.
→ Collision이 최대한 안 생기도록 Hash 함수를 설계해야 한다.
다음은 Hash function을 구현하는 대표적인 방법들이다.
Collision resolution(충돌 해결법)
initial capacity는 처음 HashMap을 생성할 때 배열의 크기
HashMap의 배열 공간의 대부분이 찼을 때 collision이 일어날 확률이 높아지므로, 어느 정도 데이터가 차면 HashMap의 배열의 크기를 늘려줘야 함.
데이터가 어느 정도 찼는지의 정도를 나타내는 것이 load factor 이고, 이 값은 0에서 1사이의 값이 될 수 있다.
HashMap을 생성할 때 이 값을 정해주지 않으면 initial capacity는 16, load factor는 0.75로 작동한다.
배열을 확장하는 것은 시간을 소요하는 작업이므로 어느 정도의 데이터를 저장할 것인지에 따라 initial capacity를 정해주는 것이 중요하다.
Java의 HashMap은 separate chaining을 사용하여 Bucket에 Linked List를 사용하거나, java8 이후에는 Red-Black Tree를 사용한다.
참고
import java.util.*;
//HashMap의 key와 value를 구현하기 위한 클래스
class Hash {
private String key;
private String value;
private Hash nextNode;
public Hash(String key, String value) {
this.key = key;
this.value = value;
this.nextNode = null;
}
public Hash(String key, String value, Hash nextNode) {
this.key = key;
this.value = value;
this.nextNode = nextNode;
}
// Getters and Setters
public String getKey() { return key; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
public Hash getNextNode() { return nextNode; }
public void setNextNode(Hash nextNode) { this.nextNode = nextNode; }
}
//Bucket Size : 초기 버킷 크기(16)
//Separate Chaining으로 충돌 해결
class CustomizedHashMap {
private Hash[] bucket;
private int bucketSize;
private int currentSize;
// 버킷 크기가 3/4가 넘을 경우 배열 크기 늘리기 위한 변수 (load factor)
private final double loadFactor = 0.75;
public CustomizedHashMap() {
this(16);
}
public CustomizedHashMap(int bucketSize) {
this.bucketSize = bucketSize;
this.bucket = new Hash[bucketSize];
this.currentSize = 0;
}
//해시 함수 (Digit Folding 방식)
private int hash(String key) {
int hashValue = 0;
int keySegment = 2;
for (int i = 0; i < key.length(); i += keySegment) {
int endIndex = Math.min(i + keySegment, key.length());
String segment = key.substring(i, endIndex);
for (char c : segment.toCharArray()) {
hashValue += (int) c;
}
}
return hashValue % bucketSize;
}
//크기 2배로 늘리기
private void resize() {
Hash[] oldBucket = bucket;
bucketSize *= 2;
bucket = new Hash[bucketSize];
currentSize = 0;
for (Hash head : oldBucket) {
Hash current = head;
while (current != null) {
put(current.getKey(), current.getValue());
current = current.getNextNode();
}
}
}
/*
put
- 현재 배열 크기가 3/4을 넘을 경우 크기 2배로 늘리기.
- 만약 이미 같은 key가 존재한다면, value 바꾸기.
- 새로운 key라면 맨 뒤에 추가하기.
*/
public void put(String key, String value) {
if (currentSize >= loadFactor * bucketSize) {
resize();
}
int index = hash(key);
if (bucket[index] == null) {
bucket[index] = new Hash(key, value);
currentSize++;
return;
}
Hash current = bucket[index];
while (current != null) {
if (current.getKey().equals(key)) {
current.setValue(value);
return;
}
current = current.getNextNode();
}
Hash newNode = new Hash(key, value);
newNode.setNextNode(bucket[index]);
bucket[index] = newNode;
currentSize++;
}
//Bucket 배열 비우기 + 현재 크기 0
public void clear() {
bucket = new Hash[bucketSize];
currentSize = 0;
}
//Linked list 형식으로 앞으로 가며 탐색하기
public String get(String key) {
int index = hash(key);
Hash current = bucket[index];
while (current != null) {
if (current.getKey().equals(key)) {
return current.getValue();
}
current = current.getNextNode();
}
return null;
}
//현재 크기 반환
public int size() {
return currentSize;
}
//같은 키가 존재한다면 true 없으면 false
public boolean containsKey(String key) {
int index = hash(key);
Hash current = bucket[index];
while (current != null) {
if (current.getKey().equals(key)) {
return true;
}
current = current.getNextNode();
}
return false;
}
//현재 크기가 0인지 확인
public boolean isEmpty() {
return currentSize == 0;
}
/*
remove
- 삭제할 키가 맨 앞인 경우
- 삭제할 키가 맨 앞이 아닌 경우 (탐색하며 찾아 삭제)
*/
public boolean remove(String key) {
int index = hash(key);
Hash current = bucket[index];
if (current == null) return false;
if (current.getKey().equals(key)) {
bucket[index] = current.getNextNode();
currentSize--;
return true;
}
while (current.getNextNode() != null) {
if (current.getNextNode().getKey().equals(key)) {
current.setNextNode(current.getNextNode().getNextNode());
currentSize--;
return true;
}
current = current.getNextNode();
}
return false;
}
//전체 키 리스트 반환
public List<String> keys() {
List<String> allKeys = new ArrayList<>();
for (int i = 0; i < bucket.length; i++) {
Hash current = bucket[i];
while (current != null) {
allKeys.add(current.getKey());
current = current.getNextNode();
}
}
return allKeys;
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
CustomizedHashMap customizedHashMap = new CustomizedHashMap();
while (true) {
System.out.print("하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : ");
String order = scanner.nextLine().trim().toLowerCase();
switch (order) {
case "put":
System.out.print("키를 입력하세요: ");
String key = scanner.nextLine();
System.out.print("값을 입력하세요: ");
String value = scanner.nextLine();
customizedHashMap.put(key, value);
System.out.println("추가 완료: (" + key + ", " + value + ")");
break;
case "get":
System.out.print("찾을 키를 입력하세요: ");
String getKey = scanner.nextLine();
String result = customizedHashMap.get(getKey);
if (result != null) {
System.out.println("값: " + result);
} else {
System.out.println("키 '" + getKey + "'를 찾을 수 없습니다.");
}
break;
case "clear":
customizedHashMap.clear();
System.out.println("모든 데이터가 삭제되었습니다.");
break;
case "size":
System.out.println("현재 크기: " + customizedHashMap.size());
break;
case "containskey":
System.out.print("확인할 키를 입력하세요: ");
String containsKey = scanner.nextLine();
boolean exists = customizedHashMap.containsKey(containsKey);
System.out.println("키 '" + containsKey + "' 존재 여부: " + exists);
break;
case "isempty":
boolean empty = customizedHashMap.isEmpty();
System.out.println("비어있는지 여부: " + empty);
break;
case "remove":
System.out.print("삭제할 키를 입력하세요: ");
String removeKey = scanner.nextLine();
customizedHashMap.remove(removeKey);
System.out.println("키 '" + removeKey + "' 삭제 완료");
break;
case "keys":
List<Integer> allKeys = customizedHashMap.keys();
if (!allKeys.isEmpty()) {
System.out.println("모든 키: " + allKeys.toString().replaceAll("[\\[\\]]", ""));
} else {
System.out.println("저장된 키가 없습니다.");
}
break;
case "exit":
System.out.println("프로그램을 종료합니다.");
scanner.close();
return;
default:
System.out.println("잘못된 명령입니다. 다시 입력해주세요.");
}
}
}
}
해시 함수 (Digit Folding)
충돌 해결 (Separate Chaining)
시간 복잡도
해시맵 키 값이 문자열이 아니라 다른 타입이라면 어떤 부분이 어떻게 바뀌는지 살펴본다.
→ Generic 타입을 사용해 코드를 변경한다.
제네릭 변환 단계별 과정
1.데이터 클래스 변경
// 기존: String 전용
class Hash {
private String key;
private String value;
private Hash nextNode;
}
// 변경: 제네릭 적용
class Hash<K, V> {
private K key;
private V value;
private Hash<K, V> nextNode;
}
클래스 시그니처 변경
// 기존
class CustomizedHashMap {
public CustomizedHashMap(int bucketSize)
}
// 변경
class CustomizedHashMap<K, V> {
public CustomizedHashMap(int bucketSize)
}
모든 함수 시그니처 업데이트
// 기존
public void put(String key, String value)
public String get(String key)
// 변경
public void put(K key, V value)
public V get(K key)
2. 해시 함수 변경 이유
기존 Digit Folding 방식은 String에만 특화된 방법이었음.
// 기존: String 전용 Digit Folding
private int hash(String key) {
int hashValue = 0;
int keySegment = 2;
for (int i = 0; i < key.length(); i += keySegment) {
int endIndex = Math.min(i + keySegment, key.length());
String segment = key.substring(i, endIndex);
for (char c : segment.toCharArray()) {
hashValue += (int) c; // String에만 있는 속성
}
}
return hashValue % bucketSize;
}
문제점:
key.length(), key.substring() → String 전용 메서드char 타입 → String 문자 전용 속성해결책: 범용 hashCode() 사용
hashCode() 메서드 활용해시 함수 변경시 나온 오류
data class 와 함수들을 generic 타입으로 변경하고 그에 맞춰 해시 함수를 변경함.
private int hash(K key) {
int hashValue = key != null ? key.hashCode() : 0;
return hashValue % bucketSize;
}
돌렸더니 오류가 나왔다.
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : put
키를 입력하세요: boostCamp
값을 입력하세요: naver
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index -12 out of bounds for length 16
이유는 "boostCamp".hashCode() = -2023192380 음수가 나왔기 때문이다.
(-2023192380) % 16 = -12 로 여전히 음수다.
String hashCode 알고리즘:
// Java String.hashCode() 공식
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
"boostCamp" 계산:
b(98) * 31^8 + o(111) * 31^7 + o(111) * 31^6 + s(115) * 31^5 +
t(116) * 31^4 + C(67) * 31^3 + a(97) * 31^2 + m(109) * 31^1 + p(112)
이 계산 결과가 32비트 정수 범위를 초과하면서 오버플로우가 발생하기 때문에 Math.abs 함수를 통해 이를 막아줘야 한다.
전체 코드
import java.util.*;
//HashMap의 key와 value를 구현하기 위한 클래스
class Hash<K, V> {
private K key;
private V value;
private Hash<K, V> nextNode;
public Hash(K key, V value) {
this.key = key;
this.value = value;
this.nextNode = null;
}
public Hash(K key, V value, Hash<K, V> nextNode) {
this.key = key;
this.value = value;
this.nextNode = nextNode;
}
// Getters and Setters
public K getKey() { return key; }
public V getValue() { return value; }
public void setValue(V value) { this.value = value; }
public Hash<K, V> getNextNode() { return nextNode; }
public void setNextNode(Hash<K, V> nextNode) { this.nextNode = nextNode; }
}
//Bucket Size : 초기 버킷 크기(16)
//Separate Chaining으로 충돌 해결
class CustomizedHashMap<K, V> {
private Hash<K, V>[] bucket;
private int bucketSize;
private int currentSize;
// 버킷 크기가 3/4가 넘을 경우 배열 크기 늘리기 위한 변수 (load factor)
private final double loadFactor = 0.75;
@SuppressWarnings("unchecked")
public CustomizedHashMap() {
this(16);
}
@SuppressWarnings("unchecked")
public CustomizedHashMap(int bucketSize) {
this.bucketSize = bucketSize;
this.bucket = (Hash<K, V>[]) new Hash[bucketSize];
this.currentSize = 0;
}
//해시 함수
private int hash(K key) {
int hashValue = key != null ? key.hashCode() : 0;
return Math.abs(hashValue) % bucketSize;
}
//크기 2배로 늘리기
@SuppressWarnings("unchecked")
private void resize() {
Hash<K, V>[] oldBucket = bucket;
bucketSize *= 2;
bucket = (Hash<K, V>[]) new Hash[bucketSize];
currentSize = 0;
for (Hash<K, V> head : oldBucket) {
Hash<K, V> current = head;
while (current != null) {
put(current.getKey(), current.getValue());
current = current.getNextNode();
}
}
}
/*
put
- 현재 배열 크기가 3/4을 넘을 경우 크기 2배로 늘리기.
- 만약 이미 같은 key가 존재한다면, value 바꾸기.
- 새로운 key라면 맨 뒤에 추가하기.
*/
public void put(K key, V value) {
if (currentSize >= loadFactor * bucketSize) {
resize();
}
int index = hash(key);
if (bucket[index] == null) {
bucket[index] = new Hash<>(key, value);
currentSize++;
return;
}
Hash<K, V> current = bucket[index];
while (current != null) {
if (Objects.equals(current.getKey(), key)) {
current.setValue(value);
return;
}
current = current.getNextNode();
}
Hash<K, V> newNode = new Hash<>(key, value);
newNode.setNextNode(bucket[index]);
bucket[index] = newNode;
currentSize++;
}
//Bucket 배열 비우기 + 현재 크기 0
@SuppressWarnings("unchecked")
public void clear() {
bucket = (Hash<K, V>[]) new Hash[bucketSize];
currentSize = 0;
}
//Linked list 형식으로 앞으로 가며 탐색하기
public V get(K key) {
int index = hash(key);
Hash<K, V> current = bucket[index];
while (current != null) {
if (Objects.equals(current.getKey(), key)) {
return current.getValue();
}
current = current.getNextNode();
}
return null;
}
//현재 크기 반환
public int size() {
return currentSize;
}
//같은 키가 존재한다면 true 없으면 false
public boolean containsKey(K key) {
int index = hash(key);
Hash<K, V> current = bucket[index];
while (current != null) {
if (Objects.equals(current.getKey(), key)) {
return true;
}
current = current.getNextNode();
}
return false;
}
//현재 크기가 0인지 확인
public boolean isEmpty() {
return currentSize == 0;
}
/*
remove
- 삭제할 키가 맨 앞인 경우
- 삭제할 키가 맨 앞이 아닌 경우 (탐색하며 찾아 삭제)
*/
public boolean remove(K key) {
int index = hash(key);
Hash<K, V> current = bucket[index];
if (current == null) return false;
if (Objects.equals(current.getKey(), key)) {
bucket[index] = current.getNextNode();
currentSize--;
return true;
}
while (current.getNextNode() != null) {
if (Objects.equals(current.getNextNode().getKey(), key)) {
current.setNextNode(current.getNextNode().getNextNode());
currentSize--;
return true;
}
current = current.getNextNode();
}
return false;
}
//전체 키 리스트 반환
public List<K> keys() {
List<K> allKeys = new ArrayList<>();
for (int i = 0; i < bucket.length; i++) {
Hash<K, V> current = bucket[i];
while (current != null) {
allKeys.add(current.getKey());
current = current.getNextNode();
}
}
return allKeys;
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
CustomizedHashMap<Integer, String> customizedHashMap = new CustomizedHashMap<>();
while (true) {
System.out.print("하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : ");
String order = scanner.nextLine().trim().toLowerCase();
switch (order) {
case "put":
System.out.print("키를 입력하세요: ");
int key = scanner.nextInt();
scanner.nextLine(); // consume newline
System.out.print("값을 입력하세요: ");
String value = scanner.nextLine();
customizedHashMap.put(key, value);
System.out.println("추가 완료: (" + key + ", " + value + ")");
break;
case "get":
System.out.print("찾을 키를 입력하세요: ");
int getKey = scanner.nextInt();
scanner.nextLine();
String result = customizedHashMap.get(getKey);
if (result != null) {
System.out.println("값: " + result);
} else {
System.out.println("키 '" + getKey + "'를 찾을 수 없습니다.");
}
break;
case "clear":
customizedHashMap.clear();
System.out.println("모든 데이터가 삭제되었습니다.");
break;
case "size":
System.out.println("현재 크기: " + customizedHashMap.size());
break;
case "containskey":
System.out.print("확인할 키를 입력하세요: ");
int containsKey = scanner.nextInt();
scanner.nextLine();
boolean exists = customizedHashMap.containsKey(containsKey);
System.out.println("키 '" + containsKey + "' 존재 여부: " + exists);
break;
case "isempty":
boolean empty = customizedHashMap.isEmpty();
System.out.println("비어있는지 여부: " + empty);
break;
case "remove":
System.out.print("삭제할 키를 입력하세요: ");
int removeKey = scanner.nextInt();
scanner.nextLine();
customizedHashMap.remove(removeKey);
System.out.println("키 '" + removeKey + "' 삭제 완료");
break;
case "keys":
List<String> allKeys = customizedHashMap.keys();
if (!allKeys.isEmpty()) {
System.out.println("모든 키: " + String.join(", ", allKeys));
} else {
System.out.println("저장된 키가 없습니다.");
}
break;
case "exit":
System.out.println("프로그램을 종료합니다.");
scanner.close();
return;
default:
System.out.println("잘못된 명령입니다. 다시 입력해주세요.");
}
}
}
}
실행 결과
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : put
키를 입력하세요: boostCamp
값을 입력하세요: naver
추가 완료: (boostCamp, naver)
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : put
키를 입력하세요: boost
값을 입력하세요: java
추가 완료: (boost, java)
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : size
현재 크기: 2
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : containsKey
확인할 키를 입력하세요: boost
키 'boost' 존재 여부: true
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : get
찾을 키를 입력하세요: boost
값: java
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : put
키를 입력하세요: boostCamp
값을 입력하세요: mobileApp
추가 완료: (boostCamp, mobileApp)
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : get
찾을 키를 입력하세요: boostCamp
값: mobileApp
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : isEmpty
비어있는지 여부: false
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : remove
삭제할 키를 입력하세요: boostCamp
키 'boostCamp' 삭제 완료
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : get
찾을 키를 입력하세요: boost
값: java
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : size
현재 크기: 1
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : clear
모든 데이터가 삭제되었습니다.
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : isEmpty
비어있는지 여부: true
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : size
현재 크기: 0
하고 싶은 동작을 입력해주세요!(put, get, clear, size, containsKey, isEmpty, remove, keys, exit) : exit
프로그램을 종료합니다.