hash는 내부적으로 배열을 사용하기 때문에 데이터를 검색할 때에 빠른 속도를 갖는다. 특정한 값을 검색할 때에는 데이터 고유의 인덱스로 접근하게 되기 때문에 average case에 대해 시간 복잡도는 O(1)이다. hash에서의 문제는 key값이 불규칙하다는 것이다.
이를 해결하기 위해 특별한 알고지름을 통해 저장할 데이터와 연관된 고유한 숫자를 만들어 낸 후 이를 인덱스로 사용한다. 특정 데이터가 저장되는 인덱스는 그 데이터만의 고유한 위치이기 때문에 삽입 연산 시 다른 데이터의 사이에 끼어들거나 삭제 시 다른 데이터로 채울 필요가 없어지기 때문에 연산에서 추가적인 비용이 발생하지 않는다.
앞서 말한 특별한 알고리즘이 바로 Hash Function이다. 이 메소드에 의해 반환된 데이터의 고유 숫자 값을 hash code라고 한다. 저장되는 값들의 key값을 hash function을 통하여 작은 범위의 값들로 바꿔준다.
그러나 어설픈 hash function을 사용할 경우 동일한 key값이 도출될 가능성이 있다. 이렇게 되면 동일한 key값에 복수 개의 데이터가 하나의 테이블에 존재할 수 있게 되는 것이다. 이것을 Collision이라고 한다.
** Collision: 서로 다른 두 개의 키가 같은 인덱스로 hashing되면 같은 곳에 저장할 수 없게되는데 이 현상을 말함.
Collision이 발생하지 않기 위해서는 좋은 hash function이 필요하다. 좋은 hash function의 조건은 다음과 같다.
Collision이 많아질 수록 검색에 필요한 시간 복잡도가 O(1)에서 O(n)으로 가까워진다. 어설픈 hash function은 hash를 hash답게 사용하지 못하게 만든다. hash table의 성능 향상에는 좋은 hash function을 선택하는 것이 필수적이라고 할 수 있다.
hashing된 인덱스에 이미 다른 값이 들어있을 경우에는 새로운 데이터를 넣기 위한 새로운 인덱스를 찾아야만 저장할 수 있는 구조이다. 그렇기 때문에 충돌의 해결은 필수이다.
충돌 해결 방법에는 2가지가 있다.
개방 주소법은 해시 충돌이 발생했을 때에 다른 해시 버킷에 해당 자료를 저장하는 방식이다. 여기서 버킷은 바구니와 같은 개념으로 데이터를 저장하기 위한 공간을 의미하고 있다. 최악의 경우에는 다른 해시 버킷을 찾지 못해 제자리로 돌아올 수도 있다. 개방 주소법에서 다른 해시 버킷을 찾는 방법에는 3가지가 존재한다.
일반적으로 Open Address 방식은 사용중인 버킷의 비율이 높아질 수록 Worst case의 발생 빈도가 증가한다. 점점 비어있는 버킷이 줄어들기 때문이다. 반면에 Separate Chaining 방식은 해시 충돌이 잘 발생하지 않도록 hash function을 조정할 수 있다면 Worst case에 가까운 경우의 빈도를 줄일 수 있다. Java 7에서는 HashMap을 Separate Chaining 방식으로 구현하고 있다. Separate Chaining 방식에는 2가지 종류가 있다.
데이터의 많고 적고의 기준은 6개와 8개이다. 7을 건너뛰고 6과 8로 정한 이유는 데이터의 양이 변화했을 때 자료구조를 변경하는 비용을 줄이기 위해서이다. 만약 초기의 데이터가 6개라면 연결 리스트로 구현할 것이다. 그러다가 데이터가 추가되어 7개가 된다면 트리로 변경시켜줘야 한다. 그러던 중에 다시 데이터가 삭제되어 6개가 된다면 연결 리스트로 자료구조를 다시 변경해야한다. 이러한 상황을 방지하기 위해 7을 건너뛰고 6과 8로 기준이 정해졌다.
두 방식 보두 최악의 경우에는 O(M)의 시간 복잡도를 가진다. 그러나 Open Address 방식은 연속된 공간에 데이터를 저장하기 때문에 캐시 효율이 높다. 따라서 데이터의 개수가 충분히 적다면 Open Address 방식을 사용하는 것이 좋다.
다른 차이점은 Open Address방식은 버킷을 계속해서 사용하는 반면 Separate Chaining은 그러지 않기 때문에 테이블의 확장을 보다 늦출 수 있다.
보조 해시 함수의 목적은 key의 해시 값을 변형하여 해시 충돌 가능성을 줄이는 것이다. Separate Chaining 방식을 사용할 때 함께 사용되며 보조 해시 함수로 최악의 경우에 가까워지는 경우를 줄일 수 있다.
해시 버킷의 개수가 적다면 메모리를 아낄 수 있지만 해시 충돌로 인해 성능의 문제가 발생하게 된다. 그래서 Hash Map은 key-value 쌍 데이터 개수가 일정 개수 이상이 되면 해시 버킷의 개수를 두배로 늘린다. 이렇게 늘리면 해시 충돌로 인한 성능 저하 문제를 어느정도 해결할 수 있다. 이때 일정 개수는 현재 데이터 개수의 75%를 의미한다. 여기서 0.75는 load factor라고 불린다.