해시 테이블은 키(key)와 값(value)으로 데이터를 저장하는 자료구조 중 하나로, 빠르게 데이터를 검색할 수 있는 것이 특징입니다. 해시 테이블이 빠른 검색 속도를 제공하는 이유는 해시 함수를 사용해 데이터의 위치를 직접 계산해 접근하기 때문입니다.
해시 테이블의 구조를 먼저 살펴보겠습니다. 키를 통해 얻고자 하는 데이터는 버킷(bucket)에 저장되어 있습니다. 버킷은 여러 개가 존재하며, 여러 버킷들은 배열을 형성합니다. 해시 함수는 키를 인자로 활용해 인덱스를 반환합니다. 이 인덱스가 곧 버킷 배열의 인덱스에 해당합니다. 키를 해시 함수에 통과시켜 원하는 버킷에 접근할 수 있는 것이죠. 이러한 해시 테이블의 핵심은 해시 함수에 있습니다. 해시 함수에 대해 자세히 알아보겠습니다.
해시 함수(Hash Function) 역할
해시 함수는 키(key)를 정수로 변환하고, 그 정수를 이용해 배열의 인덱스를 계산하는 함수입니다. 이 함수는 해시 테이블에서 데이터를 어디에 저장할지 결정하는 핵심 요소입니다.
가장 간단한 해시 함수 방식은 모듈러 연산(modular arithmetic) 입니다. 이 방식은 키를 숫자로 변환한 뒤, 배열의 크기(버킷 수) 로 나눈 나머지를 인덱스로 사용하는 방식입니다.
index = hash(key) % M
여기서 hash(key)는 키를 정수로 변환한 값이고, M은 해시 테이블(배열)의 크기입니다.
예를 들어, 키 “apple”을 해시 함수에 넣어 3이라는 숫자가 나왔다면, 내부 배열의 3번 인덱스에 “apple”과 관련된 값을 저장하게 됩니다.
충돌이 발생했을 때 두 번째 해시 함수의 값을 이용해 탐색 간격을 결정합니다. 이중 해싱의 핵심은 탐사할 해시값의 규칙성을 없애버려서 클러스터링을 최소화하는 기법입니다.
다만, 구현이 상대적으로 복잡하고 보조 해시 값이 0이 되지 않도록 주의해야 합니다.
// 충돌이 발생하면 두 번째 해시 함수의 값을 이용해 탐색 간격을 설정하는 방식
// 첫 해시 값이 152이고, 보조 해시 값이 7이라면
//
index = (hash1(key) + i * hash2(key)) % M
예를 들어, 첫 해시 값이 152이고, 보조 해시 값이 7이라면 152 -> 159 (152+1x7) -> 166(152+2x7) 순으로 버킷을 탐색합니다.
문제점
Open addressing 방식에서는 충돌이 발생했을 때 동일한 해시 테이블 내에서 다른 위치를 찾아 데이터를 저장하는데, 이 과정에서 삭제 시 단순히 해당 자리를 비워버리면, 이후 탐색 시 데이터를 찾지 못하게 되는 문제가 생깁니다.
예를 들어, 어떤 키를 검색하려고 선형 탐사를 하는 도중 중간에 빈 공간을 만나면, 탐색을 멈추고 "데이터가 없다"고 잘못 판단하게 되는 것입니다.
이를 해결하기 위해서는 삭제된 자리에 삭제 마커(예: "deleted" 표시) 를 남겨서 이후 탐색이 계속될 수 있도록 처리합니다. 하지만 이 방식도 성능 저하를 일으킬 수 있기 때문에, 일정 수준 이상 삭제가 누적되면 재해싱(rehashing) 을 고려해야 합니다.
해시테이블(Hash Table) 시간 복잡도
해시 테이블은 키(key)와 값(value)을 저장할 때, 해시 함수를 이용해 데이터를 저장할 위치를 계산함으로써 평균적으로 O(1)의 빠른 조회 속도를 자랑하는 자료구조입니다. 하지만 현실에서는 해시 함수가 충돌을 피할 수 없습니다. 서로 다른 키가 같은 인덱스를 반환하는 경우, 이를 처리하기 위해 보통 체이닝(Chaining) 같은 충돌 해결 전략을 사용합니다.
체이닝의 경우, 충돌이 발생하면 해당 버킷에 연결 리스트 형태로 데이터를 추가하게 되는데, 이 경우 최악의 경우에는 모든 데이터가 한 버킷에 연결되는 상황도 발생할 수 있습니다. 이런 경우 탐색 시 리스트를 끝까지 순회해야 하므로 시간 복잡도는 O(N)에 가까워질 수 있습니다.
그래서 충돌을 줄이기 위해 보통 다음과 같은 방식이 사용됩니다.
더 정교한 해시 함수 사용
버킷 수(테이블 크기)를 충분히 크게 설정
개방 주소법에서 충돌 분산 기법 적용 (예: 이중 해싱)
하지만 이러한 방법들은 더 많은 메모리를 요구하며, 공간 낭비를 유발할 수 있다는 단점도 함께 존재합니다.
만약 테이블이 가득 차게 되면, 해시 테이블은 보통 자동으로 크기를 두 배로 늘려 리사이징(resize)합니다. 이 과정에서 모든 데이터를 다시 해싱해야 하기 때문에, 심각한 성능 저하가 발생할 수 있습니다. 일반적으로 해시 테이블은 공간 사용률(load factor)이 70% ~ 80%에 도달하면 충돌이 급격히 증가하기 시작합니다. 이를 방지하려면 처음부터 데이터 규모를 고려하여 적절한 크기로 테이블을 설계하거나, 리사이징 비용을 감안한 구현이 필요합니다.
JavaScript의 Object와 Map
JavaScript에서는 별도의 해시 테이블 클래스를 직접 구현하지 않더라도, 내장 객체(Object)와 Map을 활용해 해시 테이블과 유사한 기능을 쉽게 사용할 수 있습니다.
Object
JavaScript의 객체({})는 본질적으로 문자열 기반 해시 테이블처럼 동작합니다.
모든 키는 내부적으로 문자열 또는 심볼(Symbol)로 변환되며, 이에 대한 값을 저장하고 빠르게 조회할 수 있습니다.
1. 해시테이블(HashTable)이란? [ HashTable(해시테이블)이란? ] 해시 테이블은 (Key, Value)로 데이터를 저장하는 자료구조 중 하나로 빠르게 데이터를 검색할 수 있는 자료구조이다. 해시 테이블이 빠른 검색속도를 제공하는 이유는 내부적으로 배열(버킷)을 사용하여 데이터를 저장하기 때문이다. 해시 테이블은 각각의 Key값에 해시함수를 적용해 배열의 고유한 index를 생성하고, 이 index를 활용해 값을 저장하거나 검색하게 된다. 여기서 실제 값이 저장되는 장소를 버킷 또는 슬롯이라고 한다. 예를 들어 우리가 (Key, Value)가 ("John Smith", "521-1234")인 데이터를 크기가 16인 해시 테이블에 저장한다고 하자. 그러면 먼저 index = hash_fun..