본 시리즈는 프로그래밍 자료구조의 개념을 정리하고 실습을 진행해보는 시리즈입니다.
Hashtable
의 기본적인 개념과 원리, 그리고 Hash 충돌이 발생했을 때 이를 해결하는 방법에 대해 설명하겠습니다.이제 본격적으로 해시테이블의 기본 개념을 살펴보겠습니다.
해시테이블(Hashtable)은 데이터를 효율적으로 저장하고 검색하기 위한 자료구조입니다.
그럼 먼저 Hash Function
이 무엇인지 살펴봅시다 !
해시 함수(Hash Function)는 임의의 크기를 가진 입력 데이터를 고정된 크기의 값으로 변환하는 함수입니다.
쉽게 이해하기 위해 간단한 예를 들어 보겠습니다.
"Hi"
라는 문자열을 해시 함수에 넣으면, 그 문자열은 다음과 같은 고정된 숫자로 변환됩니다"Hi" --> 2337
이 숫자는 Java
에서 "Hi"
라는 문자열을 해시 함수에 넣었을 때 반환되는 값(해시 코드)입니다.
Q. "Hi"는 왜 2337이 되었나요?
Java
에서는 문자열을 유니코드 값으로 처리한 뒤, 각 문자의 값을 31씩 곱하고 더하는 방식으로 해시 코드를 계산합니다.
- 예를 들어,
"Hi"
의 해시 코드는'H'(72) * 31 + 'i'(105) = 2337
입니다.- 참고로, Python의 해시 함수는 보안 강화를 위해 실행할 때마다 해시 값을 무작위로 변경하는 Hash Randomization이라는 기능을 사용합니다.
- 같은 문자열이더라도 실행할 때마다 다른 해시 값을 반환할 수 있지만, 한 실행 내에서는 일관된 해시 값을 유지합니다.
- 이 부분은 너무 복잡하게 생각할 필요 없이, 실행 중에는 같은 문자열에 대해 일관된 결과를 준다고 기억하시면 됩니다.
아무튼 어떤 종류의 데이터라도 특정 규칙에 따라 고정된 값(정수)가 나오도록 하는 함수가 바로 해시 함수이고, 그 결과가 해시 코드이다! 이것만 기억하시면 됩니다.
해시 코드(Hash Code)는 해시 함수에 의해 계산된 고정된 길이의 숫자입니다.
이렇게 계산 된 해시 코드는 일반적으로 테이블(혹은 배열)의 크기를 기준으로 나머지 연산을 통해 테이블 내의 특정 인덱스에 매핑됩니다.
2337
이라는 해시 코드가 있고, 배열의 길이가 10
이라면 10으로 나눈 나머지인 7
을 사용해서 데이터를 저장하거나 검색하는 인덱스를 결정하게 됩니다.Q. 해시 코드를 사용하는 이유는 뭔가요?
- 데이터를 직접 비교하는 대신, 해시 코드를 사용하여 더 빠르게 데이터를 탐색할 수 있기 때문입니다.
- 예를 들어, 대량의 문자열이 있을 때, 각각의 문자열을 하나하나 비교하는 대신, 해시 코드를 먼저 비교하여 빠르게 일치 여부를 판단할 수 있습니다.
- 이 덕분에 탐색 속도가 크게 향상됩니다.
해시 함수가 서로 다른 두 개 이상의 키에 대해 동일한 해시 코드를 반환할 때를 해시 충돌(Hash Collision)이라고 합니다.
"Key1"
과 "Key2"
가 있을 때, 이 두 키가 모두 동일한 해시 인덱스로 매핑된다면, 해시 충돌이 발생하게 됩니다.Q. 충돌이 발생하면 어떤 문제가 있나요?
- 해시 테이블에서 충돌이 발생하면, 동일한 인덱스에 여러 데이터가 저장되므로 데이터를 저장하거나 검색하는 과정이 복잡해집니다.
- 이를 해결하지 않으면 해시 테이블의 효율성이 크게 떨어집니다.
따라서 해시 충돌이 발생했을 때 이를 어떻게 처리할 것인지가 중요한데, 해시 충돌을 해결하는 방법은 크게 개별 체이닝(Separate Chaining)과 개방 주소법(Open Addressing) 두 가지로 나뉩니다.
Java
에서 이 방식을 채택하여 해시테이블(Set, Map)에 적용합니다.Python
이 이 방식을 채택하여 해시테이블(dict)에 적용합니다.어떤 해시 충돌 해결 방법을 선택할지는 데이터의 크기, 메모리 사용량, 성능 요구사항 등에 따라 달라집니다.
HashMap
, HashSet
과 같은 자료구조에서 사용됩니다.dict
와 같은 해시테이블에서 많이 사용됩니다.위 두 방법 중 어느 방식이 더 나을지에 대한 절대적인 정답은 없습니다.
- 그래서 각 언어와 라이브러리에서는 각자의 설계 목표에 따라 서로 다른 방식이 선택되고 최적화되었습니다.
- 예를 들어, Java는 개별 체이닝 방식으로 해시 충돌을 처리하며, 이 방법은 대규모 데이터를 다룰 때 유연한 처리를 제공합니다.
- 반면에 Python은 개방 주소법을 채택하여, 상대적으로 메모리 효율성을 극대화하는 데 중점을 둡니다.
결론적으로, 데이터의 특성과 환경에 맞춰 각각의 방법이 활용될 수 있으며, 사용자가 환경에 맞는 방식을 선택하거나 언어에서 제공하는 최적의 해시테이블을 사용하면 됩니다.
이제 해시 테이블의 기본 개념과 해시 충돌 해결 방법에 대해 어느정도 알아보았으니, Java와 Python에서 실제로 해시 테이블이 어떻게 구현되고 사용되는지 간단히 살펴보겠습니다.
Java에서 해시 테이블은 주로 HashMap
과 HashSet
과 같은 클래스에서 구현됩니다.
HashMap: Entry
라고도 불리는 키-값 쌍을 저장하는 자료구조입니다. 키를 해시 함수로 해시코드로 변환한 후, 해시코드를 이용해 데이터를 저장합니다.
HashSet: 유일한 값을 저장하기 위한 자료구조로, 내부적으로 HashMap
을 사용해 중복을 방지합니다. (쉽게 말해서 Key만 존재하는 HashMap
이라 보시면 됩니다)
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// HashMap 생성
HashMap<String, Integer> map = new HashMap<>();
// 데이터 삽입
map.put("apple", 10);
map.put("banana", 20);
// 데이터 조회
System.out.println(map.get("apple")); // 10 출력
// 데이터 삭제
map.remove("banana");
}
}
Python에서는 dict
와 set
이 해시 테이블로 구현되어 있으며, 주로 개방 주소법(Open Addressing)을 사용해 해시 충돌을 해결합니다.
dict
: 키-값 쌍을 저장하는 자료구조로, 해시 함수를 사용해 키를 인덱스로 변환하고 데이터를 저장합니다.set
: 중복되지 않는 유일한 값을 저장하는 자료구조로, 내부적으로 dict
를 사용하여 값의 존재 여부를 해시 테이블로 처리합니다.# Python dict 예시
my_dict = {"apple": 10, "banana": 20}
# 데이터 조회
print(my_dict["apple"]) # 10 출력
# 데이터 삽입
my_dict["orange"] = 30
# 데이터 삭제
del my_dict["banana"]
이번 포스팅에서는 해시 테이블의 기본 개념과 이를 구성하는 해시 함수(Hash Function), 해시 코드(Hash Code)의 원리를 다루었습니다.
또한, 해시 충돌이 발생하는 원리와 이를 해결하기 위한 두 가지 방법, 개별 체이닝(Separate Chaining)과 개방 주소법(Open Addressing)에 대해서도 알아보았습니다.
또한, 간단한 예시를 통해 Java의 HashMap
과 HashSet
, 그리고 Python의 dict
와 set
에서 해시 테이블이 어떻게 동작하는지를 간단하게 살펴보았습니다.
다음 포스팅에서는 먼저 Java에서의 해시 테이블 구현을 좀 더 구체적으로 다루어 보겠습니다.
HashMap
, HashSet
, LinkedHashMap
등의 자료구조를 비교하고, 이들이 어떻게 해시 충돌을 처리하는지, 그리고 성능 측면에서 어떤 차이가 있는지에 대해 깊이 있게 다루어볼 예정입니다.