내용 보기
작성자
관리자 (IP : 172.17.0.1)
날짜
2020-07-09 05:02
제목
[자료구조] 해시(Hash) 기본 개념과 구조, 해시 충돌해결법
Java 해쉬(Hash) 기본 개념과 구조 1. 해쉬(Hash)의 개요 앞에서 데이터를 삽입, 검색, 삭제하는데 사용되는 몇가지 자료구조를 살펴보았다. 리스트, 스택, 큐 등의 자료구조를 배열로 구현하거나 연결 리스트로 구현하는 방법을 보면 삽입과 삭제는 연결 리스트가 효율적으로 동작하고 검색은 배열이 더 효율적으로 동작하는걸 확인할 수 있다. [자료구조] Java 배열(array)과 리스트(list) 비교 배열은 내부 인덱스를 이용하여 자료의 검색이 한번에 이루어지기 때문에 빠른 검색 속도를 보이는 반면 데이터의 삽입, 삭제시 많은 데이터가 밀리거나 빈자리를 채우기 위해 이동해야 하기 때문에 많은 시간이 소요된다. 반면에 연결 리스트는 삽입, 삭제시 인근 노드들의 참조값만 수정해 줌으로써 빠른 처리가 가능했었다. 단 처음노드 마지막노드 이외의 위치에서 데이터를 삽입, 삭제할 경우나 데이터를 검색할 경우에는 해당 노드를 찾기 위하여 처음부터 순회검색을 해야하기 때문에 데이터의 수가 많아질수록 효율이 떨어질 수 밖에 없는 구조였다. 이러한 한계를 극복하기 위하여 제시된 방법이 바로 해쉬(Hash)이다. 2. 해쉬(Hash)의 기본 개념 해쉬(Hash)는 내부적으로 배열을 사용하여 데이터를 저장하기 때문에 빠른 검색 속도를 갖는다. 그리고 데이터의 삽입과 삭제시 기존 데이터를 밀어내거나 채우는 작업이 필요 없도록 특별한 알고리즘을 이용하여 데이터와 연관된 고유한 숫자를 만들어 낸뒤 이를 인덱스로 사용한다. 특정 데이터가 저장되는 인덱스를 그 데이터만의 고유한 위치이기 때문에 삽입시 다른 데이터의 사이에 끼어들거나 삭제시 다른 데이터로 채울 필요가 없으므로 삽입과 삭제시 데이터의 이동이 없도록 만들어진 구조이다. 해쉬(Hash)가 내부적으로 사용하는 배열을 Hash Table 이라고 하며 그 크기에 따라서 성능 차이가 많이 날 수 있다. 3. 해쉬(Hash) 메소드 해쉬(Hash)는 Hash Table을 이용하여 데이터를 저장한다. 이때 특별한 알고리즘을 이용하여 데이터의 고유한 숫자값을 만들어 인덱스로 사용하는데 이 알고리즘을 구현한 메소드를 Hash Method라고 하며 Hash Method에 의해 반환된 데이터 고유의 숫자 값을 Hash Code 라고 한다. Java 에서는 Object 클래스의 hashCode() 라는 메소드를 이용하여 모든 객체의 Hash Code를 쉽게 구할 수 있다. Hash Method를 구현하는 방법은 여러가지가 있지만 가장 간단한 방법으로는 나머지 연산자를 이용하는 것이다. 데이터의 고유한 hashCode 를 구한 뒤 hashCode를 테이블의 크기로 나머지 연산을 한 결과를 해당 데이터의 인덱스로 사용하는 것이다. 예를들어 "a", "b", "c"의 hashCode가 각각 97, 98 ,99 라고 하고 Hash Table의 크기가 10이라고 했을 때 테이블에 저장될 인덱스는 다음과 같다. 97 % 10 = 7 98 % 10 = 8 99 % 10 = 9 즉, Hash Table의 인덱스 7에는 "a"를 저장하고 8에는 "b", 9에는 "c"를 저장하는 방식이다. 이후에 "a"를 검색할 때에는 "a"의 hashCode를 가지고 나머지 연산을 한 결과인 7번 인덱스를 바로 참조하여 데이터를 꺼낼 수 있다. 또다른 예를 들어 보자 4, 8, 12, 16, 20, 24, 28, 32 라는 hashCode를 가진 데이터가 있다고 했을 때 데이터를 저장하기 위해 Hash Table의 크기를 8로 지정했다면 테이블에 저장될 인덱스는 다음과 같다. 4 % 8 = 4 8 % 8 = 0 12 % 8 = 4 16 % 8 = 0 20 % 8 = 4 24 % 8 = 0 28 % 8 = 4 32 % 8 = 0 데이터를 저장하기 위한 인덱스가 0번과 4번에 집중되는 것을 볼 수 있다. 이처럼 hashCode가 다르더라도 나머지 연산을 한 결과는 같을 수 있으므로 Hash Table의 동일한 인덱스에 저장을 시도하려 할 경우 문제가 발생한다. 이렇게 저장하려는 위치에 이미 다른 데이터가 있어서 저장을 할 수 없는 현상을 충돌(collision)이라고 하며 이런 충돌을 최소화 하기 위한 방법으로는 나머지 연산의 값이 최대한 중복되지 않도록 테이블의 크기를 소수(prime number)로 만드는 것이다. 다음은 8보다 큰 소수인 11을 테이블의 크기로 지정했을 때 각 데이터들이 테이블에 저장될 인덱스 값이다. 4 % 11 = 4 8 % 11 = 8 12 % 11 = 1 16 % 11 = 5 20 % 11 = 9 24 % 11 = 2 28 % 11 = 6 32 % 11 = 10 이제는 충돌이 발생하지 않는다. 하지만 Hash Table의 크기를 소수로 만드는 것은 충돌을 줄이는 방법이지 해결하기 위한 방법은 아니다. 추가적으로 10을 저장하려고 한다면 10 % 11 = 10 로 되므로 이미 10번의 인덱스에는 32가 들어있기 때문에 충돌이 발생한다. 이처럼 충돌이 발생할 경우 이를 해결하기 위한 방법이 필요하며 충돌 해결 방법으로 개방주소법과분리연결법 두가지가 있다. 4. 충돌 해결 방법 Hash Table에 이미 다른 데이터가 저장되어 있을 경우 데이터를 저장하지 못하는 충돌이 발생한다고 했다. 이를 해결하기 위한 방법으로는 개방주소법과 분리연결법이 있지만 개방주소법은 충돌이 많이 일어날 경우 심각한 성능저하가 발생할 수 있기 때문에 분리연결법에 대해서만 확인해 보도록 하겠다. 분리연결법은 Hash Table에 연결 리스트에서 사용하는 Node 객체를 저장하는 것이다. 즉, Hash Table의 셀마다 연결 리스트를 하나씩 저장하도록 하고 충돌이 발새하는 데이터는 연결 리스트의 다음 노드로 계속 추가해 가는 것이다. 이후에 데이터를 검색할 때에는 Hash Table의 인덱스를 찾은 후 셀에 연결된 리스트를 순차적으로 탐색하며 찾으려는 hashCode와 저장된 노드의 hashCode를 비교하는 것이다. 5. 분리연결법을 이용한 Hash 구현
배열로 되어있는 Hash Table안에 리스트가 있어서 조금 복잡해 보일 수 있다. Hash Table에 저장할 Entry 는 값과 다음 Entry를 가리킬 변수를 가지고 생성한다. getKey() 함수는 data 를 가지고 hashCode 를 생성하여 반환한다. 실제 Hash 에서는 특별한 알고리즘으로 hashCode를 생성하지만, 여기서는 data 값을 key로 하겠다. hashMethod() 는 key를 가지고 Hash Table에 저장할 index를 결정하여 반환한다 Hash에 데이터의 삽입은 키와 인덱스를 가지고 Hash Table에서 해당 인덱스를 확인한다. 해당 인덱스가 비어있으면 Entry를 저장하면 되고, 값이 있다면 첫번째 Entry 부터 순차적으로 탐색하여 key가 삽입될 위치를 찾아 삽입한다. (Entry 리스트는 단순연결리스트이며, 작은값부터 큰 값 순서로 정렬되어 있다.) 해당 데이터의 위치 탐색은 hashMethod()로 index를 얻은 후, 해당 index에 데이터가 있는지 탐색한다. 있으면 index를 반환하면 되고 없으면 -1 을 반환한다. 데이터의 제거를 위해서는 data에 해당하는 index의 Entry 리스트 에서 data의 앞 노드가 다음 노드를 data의 뒷 노드를 가리키게 하면 된다. Hash는 탐색이 빠른 배열의 장점과 삽입/삭제시 데이터의 밀어내기 이동이 필요없는 장점을 이용해 성능을 향상시킨 데이터 저장 방법이다. 위에서 소개한 분리연결법은 배열과 단순연결리스트를 혼합하여 데이터의 삽입/삭제/탐색의 성능을 높인다. 개방주소법은 배열만을 사용하며, 저장 인덱스가 겹칠경우 해당 인덱스의 옆 인덱스에 저장하는 방식이다. 이때 옆 인덱스와의 데이터 충돌이 또다시 일어나면 다시 옆 인덱스로 저장한다. 따라서 충돌 데이터가 많이 발생하면 성능에 심각한 문제가 발생하는 단점이 있다. |
출처1
http://hyeonstorage.tistory.com/m/post/265
출처2