해시 테이블은 어떻게 작동합니까?


494

해시 테이블의 작동 방식에 대한 설명을 찾고 있습니다.

예를 들어 키를 가져 와서 해시를 계산하고 (어떻게 설명을 찾고 있는지) 값을 저장하는 배열의 위치를 ​​알아 내기 위해 모듈로를 수행하지만 지식이 멈 춥니 다. .

누구나 프로세스를 명확히 할 수 있습니까?

편집 : 해시 코드 계산 방법에 대해 구체적으로 묻지 않고 해시 테이블 작동 방법에 대한 일반적인 개요를 제공합니다.


4
최근에는 해시 테이블 및 해당 전략 (별도 체인, 선형 프로빙, ​​이중 해싱)에 악센트를 사용하여 데이터를 저장하고 조회하는 몇 가지 방법을 설명하는 이 기사 ( en.algoritmy.net/article/50101/Hash-table )를 작성했습니다. )
malejpavouk

1
해시 테이블은 배열의 확장 버전으로 생각할 수 있는데, 이는 연속 정수 키에만 국한되지 않습니다.
user253751

1
또 다른 하나는 다음과 같습니다. Intelligentjava.wordpress.com/2016/10/19/…
nesvarbu

답변:


913

평신도의 용어로 설명합니다.

도서관에 책을 채우고 책을 채우는 것이 아니라 필요할 때 쉽게 다시 찾을 수 있기를 원한다고 가정 해 봅시다.

따라서 책을 읽고 싶은 사람이 책의 제목과 부팅 할 정확한 제목을 알고 있으면 그게 전부라고 결정합니다. 제목을 통해 사서의 도움을 받아 그 사람이 책을 쉽고 빠르게 찾을 수 있어야합니다.

어떻게 그렇게 할 수 있습니까? 글쎄, 당신은 분명히 당신이 각 책을 넣는 곳의 목록을 유지할 수 있지만, 도서관을 검색하는 것과 같은 문제가 있습니다. 당신은 목록을 검색해야합니다. 물론, 목록은 더 작고 쉽게 검색 할 수 있지만 여전히 라이브러리 (또는 목록)의 한쪽 끝에서 다른 쪽 끝까지 순차적으로 검색하고 싶지는 않습니다.

책 제목으로 한 번에 올바른 지점을 제공 할 수있는 것을 원하므로 필요한 선반으로 걸어 가서 책을 집어 올리면됩니다.

그러나 어떻게 할 수 있습니까? 글쎄, 라이브러리를 채울 때 약간의 예측과 라이브러리를 채울 때 많은 작업이 있습니다.

라이브러리를 한쪽 끝에서 다른 쪽 끝까지 채우는 대신 영리한 작은 방법을 고안하십시오. 책 제목을 가져 와서 작은 컴퓨터 프로그램을 통해 실행하십시오.이 프로그램은 선반 번호와 해당 선반의 슬롯 번호를 표시합니다. 여기가 책을 놓는 곳입니다.

이 프로그램의 장점은 나중에 사람이 책을 읽기 위해 다시 들어올 때 프로그램을 통해 제목을 다시 한 번 피드하고 원래 제공된 선반 번호와 슬롯 번호를 다시 얻는다는 것입니다. 책이있는 곳.

다른 사람들이 이미 언급 했듯이이 프로그램은 해시 알고리즘 또는 해시 계산이라고하며 일반적으로 공급 된 데이터 (이 경우 책 제목)를 가져 와서 숫자를 계산하여 작동합니다.

간단히하기 위해 각 문자와 기호를 숫자로 변환하고 모두 합산한다고 가정 해 봅시다. 실제로는 그보다 훨씬 복잡하지만 지금은 그대로 두겠습니다.

이러한 알고리즘의 장점은 동일한 입력을 반복해서 입력하면 매번 같은 숫자를 뱉어내는 것입니다.

기본적으로 해시 테이블이 작동하는 방식입니다.

기술적 인 내용은 다음과 같습니다.

첫째, 숫자의 크기가 있습니다. 일반적으로 이러한 해시 알고리즘의 출력은 일반적으로 테이블에있는 공간보다 훨씬 큰 일부 범위 내에 있습니다. 예를 들어, 도서관에 정확히 백만 권의 책을 넣을 공간이 있다고 가정 해 봅시다. 해시 계산의 출력은 0에서 10 억 사이 일 수 있으며 훨씬 더 높습니다.

그래서 우리는 무엇을합니까? 우리는 모듈러스 계산 (modulus calculation)이라는 것을 사용합니다. 이것은 기본적으로 당신이 원하는 수 (예 : 10 억 숫자)를 세었지만 훨씬 더 작은 범위 안에 머무르기를 원한다면, 당신이 다시 시작한 작은 범위의 한계에 도달 할 때마다 0이지만 큰 순서로 얼마나 멀리 있는지 추적해야합니다.

해시 알고리즘의 출력이 0에서 20 사이이고 특정 제목에서 값 17을 얻습니다. 도서관의 크기가 7 권인 경우 1, 2, 3, 4, 5, 6으로 계산하고 7에 도달하면 0부터 다시 시작합니다. 17 번 계산해야하므로 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3이며 최종 숫자는 3입니다.

물론 모듈러스 계산은 그런 식으로 수행되지 않고 나누기와 나머지로 수행됩니다. 17을 7로 나누는 나머지는 3입니다 (7은 14에서 17에 2 번 들어가고 17과 14의 차이는 3입니다).

따라서 책을 슬롯 번호 3에 넣습니다.

이것은 다음 문제로 이어집니다. 충돌. 알고리즘은 책을 간격을 두어 라이브러리를 정확하게 채울 수있는 방법이 없기 때문에 (또는 원하는 경우 해시 테이블) 항상 이전에 사용 된 숫자를 계산하게됩니다. 도서관의 의미에서 책장에 넣고 싶은 선반 번호와 슬롯 번호에 도달하면 이미 책이 있습니다.

데이터를 또 다른 계산으로 실행하여 테이블의 다른 지점을 가져 오거나 ( 이중 해싱 ) 주어진 공간에 가까운 공간을 찾는 것 (예 : 슬롯을 가정 한 이전 책 바로 옆)을 포함한 다양한 충돌 처리 방법이 있습니다. 선형 프로빙 이라고도 함 ). 이것은 나중에 책을 찾으려고 할 때 약간의 파고가 있다는 것을 의미하지만, 도서관의 한쪽 끝에서 시작하는 것보다 여전히 낫습니다.

마지막으로 라이브러리가 허용하는 것보다 많은 책을 라이브러리에 넣을 수 있습니다. 다시 말해, 더 큰 라이브러리를 만들어야합니다. 라이브러리의 정확한 지점은 라이브러리의 정확한 현재 크기를 사용하여 계산되었으므로 라이브러리의 크기를 조정하면 계산을 수행 한 후 해당 지점을 찾기 위해 모든 책에 대해 새로운 지점을 찾아야 할 수도 있습니다. 변경되었습니다.

이 설명이 버킷과 함수보다 지구에 약간 더 가깝기를 바랍니다. :)


훌륭한 설명 감사합니다. 4.x .Net 프레임 워크에서 구현되는 방법에 대한 자세한 기술 정보를 어디서 찾을 수 있는지 알고 있습니까?
Johnny_D

아니요, 숫자 일뿐입니다. 각 선반과 슬롯의 번호는 0 또는 1에서 시작하여 해당 선반의 각 슬롯에 대해 1 씩 증가한 다음 다음 선반에서 계속 번호를 매 깁니다.
Lasse V. Karlsen

2
'테이블에서 다른 지점을 얻기 위해 데이터를 또 다른 계산으로 실행하는 것을 포함하여 다양한 충돌 처리 방법이 존재합니다.'-다른 계산의 의미는 무엇입니까? 또 다른 알고리즘입니까? 책 이름에 따라 다른 숫자를 출력하는 다른 알고리즘을 사용한다고 가정 해 봅시다. 그런 다음 나중에 그 책을 찾으려면 어떤 알고리즘을 사용해야하는지 어떻게 알 수 있습니까? 원하는 제목의 책을 ​​찾을 때까지 첫 번째 알고리즘, 두 번째 알고리즘 등을 사용합니까?
user107986

1
@KyleDelaney : 폐쇄 해싱의 경우 아니요 (대체 버킷을 찾아 충돌을 처리하는 경우, 메모리 사용량은 고정되어 있지만 버킷 전체에서 검색하는 데 더 많은 시간을 소비 함). 들어 개방 체인 일명 해시 (의도적으로 약간의 적수 / 해커에 의해 충돌 만들어진 끔찍한 해시 함수 또는 입력) 병적 인 경우에 당신은 해시 버킷이 비어 가장 끝낼 수 없었다, 그러나 총 메모리 사용량은 더 나쁘다 - 단지 더 포인터 대신에 NULL 데이터를 유용하게 색인화합니다.
Tony Delroy

3
@KyleDelaney : 댓글을 받으려면 "@Tony"가 필요합니다. 체인에 대해 궁금한 것 같습니다. 값 노드 A{ptrA, valueA}, B{ptrB, valueB}, C{ptrC, valueC}3 개와 버킷 3 개가 있는 해시 테이블 이 있다고 가정 합니다 [ptr1, ptr2, ptr3]. 삽입시 충돌이 있는지 여부에 관계없이 메모리 사용량이 고정됩니다. 충돌이 없을 수 있습니다 : A{NULL, valueA} B{NULL, valueB} C{NULL, valueC}[&A, &B, &C], 또는 모든 충돌 A{&B, valueA} B{&C, valueB}, C{NULL, valueC}[NULL, &A, NULL]: NULL 버킷이 "폐기"됩니까? 킨다. 사용 된 총 메모리가 동일합니다.
Tony Delroy 2013

104

사용법 및 Lingo :

  1. 해시 테이블 은 데이터 (또는 레코드)를 빠르게 저장하고 검색하는 데 사용됩니다.
  2. 레코드는 해시 키를 사용하여 버킷 에 저장됩니다
  3. 해시 키 는 레코드에 포함 된 선택한 값 ( 값)에 해싱 알고리즘을 적용하여 계산됩니다 . 이 선택된 값은 모든 레코드에 공통 값이어야합니다.
  4. 버킷 에는 특정 순서로 구성된 여러 레코드가있을 수 있습니다.

실제 예 :

1803 년에 설립되었으며 컴퓨터 기술이 부족한 Hash & Co. 는 약 30,000 명의 고객에 대한 상세 정보 (기록)를 보관하기 위해 총 300 개의 파일 캐비닛을 보유하고있었습니다. 각 파일 폴더는 클라이언트 번호 (0-29,999의 고유 번호)로 명확하게 식별되었습니다.

당시의 사무원은 근무하는 직원의 고객 기록을 신속하게 가져 와서 저장해야했습니다. 직원들은 해싱 방법을 사용하여 레코드를 저장하고 검색하는 것이 더 효율적이라고 결정했습니다.

고객 기록을 제출하기 위해 사무원은 폴더에 기록 된 고유 고객 번호를 사용합니다. 이 클라이언트 번호를 사용하면 포함 된 파일 캐비닛을 식별하기 위해 해시 키 를 300 씩 변조합니다 . 파일 캐비닛을 열면 클라이언트 번호별로 많은 폴더가 포함되어 있음을 알 수 있습니다. 올바른 위치를 식별 한 후에는 간단히 해당 위치로 이동합니다.

고객 기록을 검색하기 위해 서류 제출시 사무원에게 고객 번호가 부여됩니다. 이 고유 한 클라이언트 번호 ( 해시 키 )를 사용하여 클라이언트 폴더가있는 파일 캐비넷을 판별하기 위해 300으로 변조합니다. 그들이 파일 캐비닛을 열었을 때 클라이언트 번호별로 많은 폴더가 포함되어 있음을 발견했습니다. 레코드를 검색하면 클라이언트 폴더를 빠르게 찾아서 검색합니다.

실제 예에서 버킷파일 캐비닛 이며 레코드파일 폴더 입니다.


기억해야 할 중요한 점은 컴퓨터 (및 해당 알고리즘)가 문자열보다 숫자를 더 잘 처리한다는 것입니다. 따라서 인덱스를 사용하여 큰 배열에 액세스하는 것이 순차적으로 액세스하는 것보다 훨씬 빠릅니다.

Simon이 내가 매우 중요하다고 생각한 것처럼 해싱 부분은 큰 공간 (임의의 길이, 일반적으로 문자열 등)을 변환하고 색인을 위해 작은 크기의 공간 (일반적으로 숫자, 알려진 숫자)으로 매핑하는 것입니다. 기억해야 할 것이 매우 중요합니다!

따라서 위의 예에서 30,000 개의 가능한 클라이언트가 더 작은 공간에 매핑되었습니다.


이것의 주요 아이디어는 일반적으로 시간이 많이 걸리는 실제 검색 속도를 높이기 위해 전체 데이터 세트를 세그먼트로 나누는 것입니다. 위의 예에서 300 개의 파일 캐비닛 각각은 (통계적으로) 약 100 개의 레코드를 포함합니다. 100 개의 레코드를 통해 (주문에 관계없이) 검색하는 것이 30,000을 처리하는 것보다 훨씬 빠릅니다.

일부는 실제로 이미이 작업을 수행 한 것을 알 수 있습니다. 그러나 해시 키를 생성하기 위해 해싱 방법을 고안하는 대신 대부분의 경우 단순히 성의 첫 글자를 사용합니다. 따라서 각각 A부터 Z까지의 문자를 포함하는 26 개의 파일 캐비닛이있는 경우 이론적으로 데이터를 세그먼트 화하고 파일 정리 및 검색 프로세스를 향상 시켰습니다.

도움이 되었기를 바랍니다,

복숭아!


2
가변적으로 "공개 주소 지정"또는 "폐쇄 주소 지정"(예, 슬프지만 사실) 또는 "체인"이라고하는 특정 유형의 해시 테이블 충돌 방지 전략을 설명합니다. 리스트 버킷을 사용하지 않고 아이템을 "인라인"으로 저장하는 또 다른 유형이 있습니다.
Konrad Rudolph

2
훌륭한 설명. 각 파일 캐비닛은 평균적으로 약 100레코드 (30k 레코드 / 300 캐비닛 = 100)를 포함합니다. 편집 할 가치가 있습니다.
Ryan Tuck

@TonyD,이 사이트 sha-1 온라인으로 이동 TonyD하여 텍스트 필드에 입력 한 SHA-1 해시를 생성하십시오 . 당신은 다음과 같은 무언가의 생성 된 값으로 끝날 것입니다 e5dc41578f88877b333c8b31634cf77e4911ed8c. 이것은 16 진수 160 비트 (20 바이트)에 지나지 않습니다. 그런 다음이를 사용하여 레코드를 저장하는 데 사용할 버킷 (한정 수량)을 결정할 수 있습니다.
Jeach

@TonyD, 충돌하는 문제에서 "해시 키"라는 용어가 어디에 있는지 잘 모르겠습니다. 그렇다면 둘 이상의 위치를 ​​지정하십시오. 아니면 Wikipedia와 같은 다른 사이트에서는 "해시 값, 해시 코드, 해시 합계 또는 단순히 해시"를 사용하는 반면 "우리"는 "해시 키"라는 용어를 사용하고 있습니까? 그렇다면 그룹 또는 조직 내에서 사용 된 용어가 일치하는 한 누가 관심을 갖습니다. 프로그래머는 종종 "핵심"용어를 사용합니다. 개인적으로 또 다른 좋은 옵션은 "해시 가치"라고 주장합니다. 그러나 "해시 코드, 해시 합계 또는 단순히 해시"를 사용하지 않을 것입니다. 단어가 아닌 알고리즘에 집중하십시오!
Jeach

2
@TonyD, 나는 텍스트를 " 해시 키 를 300으로 모듈화 할 것"으로 변경하여 모든 사람들이 더 깨끗하고 깨끗해지기를 바랐습니다. 감사!
Jeach

64

이것은 이론의 매우 깊은 영역으로 밝혀졌지만 기본 개요는 간단합니다.

본질적으로 해시 함수는 하나의 공백 (임의의 길이의 문자열)에서 물건을 가져 와서 인덱싱에 유용한 공간 (예 : 부호없는 정수)에 매핑하는 함수 일뿐입니다.

해시해야 할 공간이 적다면 정수로 해석하는 것만으로도 벗어날 수 있습니다 (예 : 4 바이트 문자열)

그러나 일반적으로 더 큰 공간이 있습니다. 키로 허용하는 것의 공간이 uint32 또는 기타 색인을 생성하는 데 사용하는 것의 공간보다 크면 각각에 대해 고유 한 값을 가질 수 없습니다. 둘 이상의 것들이 동일한 결과로 해시되면 중복을 적절한 방식으로 처리해야합니다 (일반적으로 충돌이라고하며 처리 방법 또는 처리하지 않는 방식은 현재 상태에 따라 조금씩 다름) 해시 사용).

이것은 동일한 결과를 얻지 않기를 원하며 해시 함수가 빠르기를 원할 수도 있습니다.

이 두 속성 (및 다른 속성)의 균형을 유지하면 많은 사람들이 바쁘게 지 냈습니다!

실제로는 일반적으로 응용 프로그램에 잘 작동하는 것으로 알려진 기능을 찾아서 사용할 수 있어야합니다.

이제이 작업을 해시 테이블로 만들려면 메모리 사용에 신경 쓰지 않았다고 상상해보십시오. 그런 다음 인덱싱 세트 (예 : 모든 uint32)만큼 배열을 만들 수 있습니다. 테이블에 무언가를 추가하면 키를 해시하고 해당 인덱스의 배열을 봅니다. 거기에 아무것도 없다면, 거기에 가치를 두십시오. 이미 무언가가 있다면,이 새로운 항목을 해당 주소에있는 것들의 목록에 추가하여 어떤 항목이 실제로 어떤 키에 속하는지를 찾기에 충분한 정보 (원래 키 또는 영리한 것)와 함께 추가하십시오.

오래 갈수록 해시 테이블 (배열)의 모든 항목이 비어 있거나 하나의 항목 또는 항목 목록을 포함합니다. 검색은 배열에 색인을 생성하고 값을 반환하거나 값 목록을 걷고 올바른 값을 반환하는 것처럼 간단합니다.

물론 실제로는 이렇게 할 수 없으며 너무 많은 메모리를 낭비합니다. 따라서 희소 배열을 기반으로 모든 작업을 수행합니다 (실제로 사용되는 항목은 다른 항목은 암시 적으로 null입니다).

이 작업을 개선하기위한 많은 구성표와 요령이 있지만 이것이 기본입니다.


1
죄송합니다.이 질문은 오래된 질문 / 답변이지만이 마지막 요점을 이해하려고 노력했습니다. 해시 테이블에는 O (1) 시간 복잡성이 있습니다. 그러나 희소 배열을 사용하면 값을 찾기 위해 이진 검색을 수행하지 않아도됩니까? 이 시점에서 시간 복잡성이 O (log n)가되지 않습니까?
herbrandson

@herbrandson : 아니요. 희소 배열은 단순히 값으로 채워진 인덱스가 거의 없음을 의미합니다. 키에서 계산 한 해시 값의 특정 배열 요소에 직접 인덱스 할 수 있습니다. 그럼에도 불구하고 Simon이 설명하는 희소 한 배열 구현은 매우 제한된 상황 에서만 제각각입니다 . 버킷 크기가 메모리 페이지 크기의 순서 일 때 (예 int: 1-in-1000 sparseness 및 4k page의 키 = 대부분의 페이지 터치) OS는 주소 공간이 충분할 때 모든 0 페이지를 효율적으로 처리하므로 (사용하지 않는 모든 버킷 페이지는 백업 메모리가 필요하지 않습니다.)
Tony Delroy

@TonyDelroy-그것은 너무 단순화 된 것이지만 아이디어는 실제 구현이 아닌 그 이유와 이유에 대한 개요를 제공하는 것이 었습니다. 후자의 세부 사항은 확장에서 끄덕임에 따라 더 미묘합니다.
simon

48

많은 답변이 있지만 그중 어느 것도 시각적이지 않으며 해시 테이블은 시각화 할 때 쉽게 "클릭"할 수 있습니다.

해시 테이블은 종종 연결된 목록의 배열로 구현됩니다. 사람들의 이름을 저장하는 테이블을 상상하면 몇 번의 삽입 후에 아래와 같이 메모리에 배치 될 수 있습니다. 여기서 ()-닫힌 숫자는 텍스트 / 이름의 해시 값입니다.

bucket#  bucket content / linked list

[0]      --> "sue"(780) --> null
[1]      null
[2]      --> "fred"(42) --> "bill"(9282) --> "jane"(42) --> null
[3]      --> "mary"(73) --> null
[4]      null
[5]      --> "masayuki"(75) --> "sarwar"(105) --> null
[6]      --> "margaret"(2626) --> null
[7]      null
[8]      --> "bob"(308) --> null
[9]      null

몇 가지 사항 :

  • 배열 항목 (인덱스는 각각 [0], [1]...)를로 알려져 버킷 및 시작 - 비어 -의 연결리스트 (일명 요소 ,이 예에서 - 사람의 이름 )
  • 각각의 값은 (예를 들어, "fred"해시가 42)에서 연결된 통 [hash % number_of_buckets]42 % 10 == [2]; %는 IS 나머지 연산자 버킷의 개수로 나눈 나머지는 -
  • 복수의 데이터 값이있다 충돌 에서와 같은 통에서 연결되어, 주로 이들의 해시 값은 모듈로 연산 한 후 충돌 (예 있으므로 42 % 10 == [2], 및 9282 % 10 == [2]) 만, 해시 값이 동일 (예를 들어, 때때로 때문에 "fred""jane"모두 해시 도시 42이상)
    • 대부분의 해시 테이블은 해시 대상 버킷의 링크 된 목록에서 이미 찾거나 삽입되는 값의 전체 값 (여기서는 텍스트)을 비교하여 성능이 약간 저하되었지만 기능적 혼란이없는 충돌을 처리합니다.

연결된 목록 길이는 값 수가 아닌로드 팩터와 관련이 있습니다.

테이블 크기가 커지면 위와 같이 구현 된 해시 테이블은 자체적으로 크기를 조정하는 경향이 있습니다 (즉, 더 큰 버킷 배열을 생성하고 그로부터 새로운 / 업데이트 된 링크 목록을 생성하고 기존 배열을 삭제) 버킷에 대한 값의 비율을 유지합니다 (일명 로드 계수 0.5 내지 1.0의 범위에서) 대체로.

Hans는 아래 주석에서 다른 부하 계수에 대한 실제 공식을 제공하지만, 표시 값에 대해서는 부하 계수 1 및 암호화 강도 해시 함수를 사용하여 버킷의 1 / e (~ 36.8 %)가 비어 있고 다른 1 / e (~ 36.8 %)는 1 / (2e) 또는 ~ 18.4 % 두 요소, 1 / (3! e) 약 6.1 % 세 요소, 1 / (4! e) 또는 ~ 1.5 % 네 요소, 1 / (5! e) ~ .3 %에는 5 개 등이 있습니다.-비어 있지 않은 버킷의 평균 체인 길이는 테이블에 몇 개의 요소가 있는지에 관계없이 ~ 1.58입니다 (즉, 100 개의 요소와 100 개의 버킷이 있는지 또는 1 억 개가 있는지) 조회 및 삽입 / 삭제가 O (1) 일정 시간 작업 이라고 말하는 이유 입니다.

해시 테이블이 키를 값과 연관시키는 방법

위에서 설명한 해시 테이블 구현을 고려할 때 필드 struct Value { string name; int age; };만 보며 name(연령 무시) 등호 비교 및 ​​해시 함수와 같은 값 유형을 작성하고 멋진 일이 발생 한다고 상상할 수 있습니다 . 테이블에서 Value와 같이 레코드 를 저장할 수 있습니다 {"sue", 63}그런 다음 나중에 나이를 모른 채 "고소"를 검색하고 저장된 값을 찾고 나이를 복구하거나 업데이트합니다.
생일 축하합니다-해시 값을 흥미롭게 변경하지 않으므로 Sue의 레코드를 다른 레코드로 옮길 필요가 없습니다. 버킷.

이 작업을 수행 할 때 해시 테이블을 연관 컨테이너 ( 일명 map )로 사용하며, 저장하는 값은 (이름)와 여전히 혼란스러워하는 하나 이상의 다른 필드 ( ( 내 예에서는 나이 만). 맵으로 사용되는 해시 테이블 구현을 해시 맵이라고 합니다.

이것은 "sue"와 같은 불연속 값을 저장 한이 답변의 앞부분의 예와 대조됩니다.이 값은 자체 키로 생각할 수 있습니다. 이러한 종류의 사용법을 해시 세트라고 합니다.

해시 테이블을 구현하는 다른 방법이 있습니다

모든 해시 테이블이 별도의 체인으로 알려진 링크 된 목록을 사용 하는 것은 아니지만 주로 대체 주소 해싱 (일명 개방 주소 지정 ) , 특히 지우기 작업이 지원되는 경우 충돌이 발생하기 쉬운 키 / 해시 함수.


해시 함수에 대한 몇 마디

강력한 해싱 ...

일반적으로 최악의 충돌을 최소화하는 해시 함수의 작업은 항상 동일한 키에 대해 동일한 해시 값을 생성하면서 해시 테이블 버킷 주위에 키를 무작위로 효과적으로 분사하는 것입니다. 키의 어느 곳에서든 1 비트 만 변경해도 결과 해시 값에서 비트의 절반 이상이 이상적으로 무작위로 플립됩니다.

이것은 일반적으로 수학에 너무 복잡하여 조정하기가 어렵습니다. 가장 확장 가능하거나 캐시 친화적이지 않지만 본질적으로 우아함 (일회용 패드를 사용한 암호화와 같은)을 이해하기 쉬운 방법에 대해 언급하겠습니다. 64 비트를 해싱한다고 가정 해보십시오. double각 256 개의 난수 (아래 코드) 각각에 8 개의 테이블을 만든 다음 각 double메모리 의 8 비트 / 1 바이트 슬라이스 를 사용하여 다른 테이블에 색인을 생성 할 수 있습니다. 당신이 찾는 임의의 숫자. 이 방법 double을 사용하면 테이블의 하나에서 다른 임의의 숫자가 조회되고 완전히 상관되지 않은 최종 값 이 결과의 임의의 위치에서 변경되는 이진 숫자 의미로 쉽게 볼 수 있습니다.

// note caveats above: cache unfriendly (SLOW) but strong hashing...
size_t random[8][256] = { ...random data... };
const char* p = (const char*)&my_double;
size_t hash = random[0][p[0]] ^ random[1][p[1]] ^ ... ^ random[7][p[7]];

약하지만 종종 빠른 해싱 ...

많은 라이브러리의 해싱 함수는 변경되지 않은 ( 사소한 또는 신원 해시 함수 라고도 함) 정수를 전달합니다 . 위에서 설명한 강력한 해싱과는 다른 극단입니다. 신원 해시는 매우최악의 경우 충돌이 발생하기 쉽지만, 아마도 약간의 공백이있는 경향이있는 정수 키의 경우에는 임의의 해싱 잎보다 비어있는 채로 연속 버킷에 매핑됩니다 (우리의 ~ 36.8 앞서 언급 한로드 팩터 1에서의 %)에 의해, 랜덤 매핑에 의해 달성되는 것보다 충돌이 적고 충돌 요소의 링크 된리스트가 적어진다. 또한 강력한 해시를 생성하는 데 걸리는 시간을 절약하고 키를 순서대로 조회하면 메모리 근처의 버킷에서 발견되어 캐시 적중률이 향상됩니다. 키 가 잘 증가 하지 않으면 무작위로 배치되어 버킷에 배치를 완전히 무작위 화하기 위해 강력한 해시 기능이 필요하지 않을 것이라는 희망이 있습니다.


6
환상적인 답변입니다.
CRThaze

@Tony Delroy 놀라운 답변에 감사드립니다. 그래도 여전히 내 마음에 하나의 열린 지점이 있습니다. 1 억 개의 버킷이 있더라도 조회 시간은로드 팩터 1과 암호화 강도 해시 함수를 갖는 O (1)이 될 것입니다. 그러나 1 억 번의 올바른 버킷을 찾는 것은 어떻습니까? 모든 버킷을 정렬 했더라도 O (log100.000.000)가 아닙니까? 버킷을 어떻게 O (1) 찾을 수 있습니까?
selman

@selman : 귀하의 질문에 O (log100,000,000)라고 생각하는 이유를 설명하는 많은 세부 정보가 제공되지 않지만 "모든 버킷이 정렬되어 있어도"라고 말하면 해시 테이블 버킷의 값을 명심하십시오. 하는 적이 있는 양동이는 키에 해시 함수를 적용하여 결정되는 값이 나타납니다 : 일반적인 의미에서 "분류하지 않습니다." 복잡성을 O (log100,000,000)라고 생각하면 정렬 된 버킷을 통해 이진 검색을 수행한다고 상상할 수 있지만 해싱이 작동하는 방식은 아닙니다. 다른 답변을 몇 개 읽고 더 이해하기 시작하는지 확인하십시오.
Tony Delroy

@TonyDelroy 실제로 "모듬 양동이"는 제가 생각하는 최고의 시나리오입니다. 따라서 O (log100,000,000)입니다. 그러나 그렇지 않은 경우 응용 프로그램은 어떻게 수백만의 관련 버킷을 찾을 수 있습니까? 해시 함수가 어떻게 든 메모리 위치를 생성합니까?
selman

1
@selman : 컴퓨터 메모리는 일정한 시간의 "무작위 접근"을 허용하기 때문에 : 메모리 주소를 계산할 수 있다면 배열의 다른 부분에서 메모리에 접근 할 필요없이 메모리 내용을 검색 할 수 있습니다. 따라서 첫 번째 버킷, 마지막 버킷 또는 그 사이의 버킷에 액세스하든 CPU L1 / L2 / L3 메모리 캐싱에 영향을 받지만 동일한 성능 특성을 갖습니다. 최근에 액세스했거나 우연히 근처에있는 버킷에 신속하게 다시 액세스 할 수 있도록 지원하며 빅 분석에서는 무시할 수 있습니다).
Tony Delroy

24

여러분은 이것을 완전히 설명하는 데 매우 가깝지만 몇 가지 사항이 누락되었습니다. 해시 테이블은 단지 배열입니다. 배열 자체는 각 슬롯에 무언가를 포함합니다. 최소한이 슬롯에 해시 값 또는 값 자체를 저장합니다. 이 외에도이 슬롯에 충돌 한 연결 / 체인 값 목록을 저장하거나 공개 주소 지정 방법을 사용할 수 있습니다. 이 슬롯에서 검색하려는 다른 데이터에 대한 포인터를 저장할 수도 있습니다.

해시 값 자체는 일반적으로 값을 배치 할 슬롯을 나타내지 않습니다. 예를 들어, 해시 값은 음의 정수 값일 수 있습니다. 분명히 음수는 배열 위치를 가리킬 수 없습니다. 또한 해시 값은 사용 가능한 슬롯보다 여러 번 큰 경향이 있습니다. 따라서 값을 입력해야하는 슬롯을 파악하기 위해 해시 테이블 자체에서 다른 계산을 수행해야합니다. 이것은 다음과 같은 계수 수학 연산으로 수행됩니다.

uint slotIndex = hashValue % hashTableSize;

이 값은 값이 들어갈 슬롯입니다. 열린 주소 지정에서 슬롯에 다른 해시 값 및 / 또는 다른 데이터가 이미 채워져 있으면 다음 슬롯을 찾기 위해 계수 작업이 다시 한 번 실행됩니다.

slotIndex = (remainder + 1) % hashTableSize;

슬롯 인덱스를 결정하는 다른 고급 방법이있을 수 있지만 이것이 내가 본 일반적인 방법입니다 ... 더 나은 성능을 가진 다른 사람들에게 관심이 있습니다.

모듈러스 방법을 사용하면 크기가 1000 인 테이블이 있으면 1과 1000 사이의 해시 값이 해당 슬롯으로 이동합니다. 음수 값과 1000보다 큰 값은 잠재적으로 충돌하는 슬롯 값입니다. 이러한 일이 발생할 가능성은 해시 방법과 해시 테이블에 추가하는 총 항목 수에 따라 다릅니다. 일반적으로 해시 테이블의 크기를 추가하여 총 값 수는 크기의 약 70 %에 불과하도록하는 것이 가장 좋습니다. 해시 함수가 균일 한 분산 작업을 수행하는 경우 일반적으로 버킷 / 슬롯 충돌이 거의 발생하지 않으며 조회 및 쓰기 작업 모두에서 매우 빠르게 수행됩니다. 더할 총 값의 수를 미리 알 수없는 경우, 어떤 방법 으로든 추측 해보십시오.

이것이 도움이 되었기를 바랍니다.

PS-C #에서이 GetHashCode()방법은 속도가 느리고 테스트 한 많은 조건에서 실제 값 충돌이 발생합니다. 정말 재미있게, 자신의 해시 함수를 작성하고 해시하는 특정 데이터에 충돌하지 않도록 해보십시오. GetHashCode보다 빠르게 실행되며 상당히 균일하게 배포됩니다. int 크기의 해시 코드 값 대신 long을 사용 하여이 작업을 수행했으며 충돌이없는 해시 테이블의 최대 3,200 만 개의 전체 해시 값에서 꽤 잘 작동했습니다. 불행히도 코드가 고용주에게 속한 코드를 공유 할 수는 없지만 특정 데이터 도메인에서 코드가 가능하다는 것을 알 수 있습니다. 이것을 달성 할 수 있으면 해시 테이블은 매우 빠릅니다. :)


나는 게시물이 꽤 오래되었다는 것을 알고 있지만 누군가 (리마인더 + 1)이 여기에서 의미하는 것을 설명 할 수 있습니다.
Hari

3
@Hari remainder는 원래의 모듈로 계산 결과를 나타내며 사용 가능한 다음 슬롯을 찾기 위해 1을 더합니다.
x4nd3r

"배열 자체에는 각 슬롯에 무언가가 포함됩니다. 최소한이 슬롯에 해시 값 또는 값 자체를 저장합니다." - "슬롯"(버킷)이 값을 전혀 저장하지 않는 것이 일반적입니다. 개방형 주소 지정 구현은 종종 NULL 또는 슬롯 / 버킷에 직접 값이없는 링크 된 목록의 첫 번째 노드에 대한 포인터를 저장합니다. "다른 사람에게 관심이있을 것" -설명하는 "+1"을 선형 프로빙 , 더 나은 성능 : 2 차 프로빙 입니다. "일반적으로 버킷 / 슬롯 충돌이 거의 또는 전혀 발생하지 않음" -@ 70 % 용량, ~ 12 % 슬롯, 2 개 값, ~ 3 % 3 ....
Tony Delroy

"int 크기의 해시 코드 값 대신 long을 사용하여이 작업을 수행했으며 충돌이없는 해시 테이블의 최대 3 천 2 백만 개의 전체 해시 값에서 상당히 잘 작동했습니다."- 일반적 으로 키 값이 버킷 수보다 훨씬 큰 범위에서 무작위로 무작위로 발생하는 경우 에는 불가능 합니다. 고유 한 해시 값을 갖는 것이 종종 쉽지만 ( long해시 값에 대한 이야기는 그것이 달성 한 것을 암시합니다), mod / % 작업이 아닌 후에 해시 테이블에서 충돌하지 않도록 하십시오 (일반적인 경우) ).
Tony Delroy

(모든 충돌을 피하는 것을 완벽한 해싱이라고 합니다. 일반적으로 사전에 알려진 수십 또는 수천 개의 키에 유용합니다. gperf는 이러한 해시 함수를 계산하는 도구의 예입니다. 상황-예를 들어, 키가 메모리 풀의 객체에 대한 포인터로 상당히 가득 차고 각 포인터가 고정 된 거리를 유지하면 포인터를 해당 거리로 나누고 인덱스를 약간 작은 배열로 효과적으로 만들 수 있습니다. 충돌).
토니 델로이

17

이것이 내 이해에서 작동하는 방식입니다.

다음은 예입니다. 전체 테이블을 일련의 버킷으로 묘사하십시오. 영숫자 해시 코드로 구현하고 알파벳의 각 문자에 대해 하나의 버킷이 있다고 가정하십시오. 이 구현은 해시 코드가있는 각 항목을 해당 버킷에 특정 문자로 시작합니다.

200 개의 객체가 있지만 그 중 15 개만 문자 'B'로 시작하는 해시 코드를 가지고 있다고 가정 해 봅시다. 해시 테이블은 200 개의 객체가 아니라 'B'버킷에서 15 개의 객체 만 검색하고 검색하면됩니다.

해시 코드를 계산하는 한 마법은 없습니다. 목표는 다른 객체가 다른 코드를 반환하고 동일한 객체가 동일한 코드를 반환하도록하는 것입니다. 모든 인스턴스에 대해 항상 해시 코드와 동일한 정수를 반환하는 클래스를 작성할 수 있지만 하나의 거대한 버킷이되기 때문에 해시 테이블의 유용성을 본질적으로 파괴합니다.


13

짧고 달다:

해시 테이블은 배열을 마무리하고 호출 할 수 있습니다 internalArray. 이런 식으로 항목이 배열에 삽입됩니다.

let insert key value =
    internalArray[hash(key) % internalArray.Length] <- (key, value)
    //oversimplified for educational purposes

때로는 두 개의 키가 배열의 동일한 인덱스로 해시되어 두 값을 모두 유지하려고합니다. 두 개의 값을 동일한 인덱스에 저장하고 싶습니다 internalArray. 연결된 목록의 배열을 만들어 코딩하기가 쉽습니다.

let insert key value =
    internalArray[hash(key) % internalArray.Length].AddLast(key, value)

따라서 해시 테이블에서 항목을 검색하려면 다음과 같이 쓸 수 있습니다.

let get key =
    let linkedList = internalArray[hash(key) % internalArray.Length]
    for (testKey, value) in linkedList
        if (testKey = key) then return value
    return null

삭제 작업은 작성하는 것만 큼 간단합니다. 알다시피, 링크 된 목록의 배열에서 삽입, 조회 및 제거는 거의 O (1)입니다.

internalArray의 용량이 약 85 %에 도달하면 내부 배열의 크기를 조정하고 모든 항목을 기존 배열에서 새 배열로 옮길 수 있습니다.


11

그것보다 훨씬 간단합니다.

해시 테이블은 키 / 값 쌍을 포함하는 벡터 의 배열 (일반적으로 희소 한 것)에 지나지 않습니다 . 이 배열의 최대 크기는 일반적으로 해시 테이블에 저장되는 데이터 유형에 대해 가능한 값 세트의 항목 수보다 작습니다.

해시 알고리즘은 배열에 저장 될 항목의 값을 기반으로 해당 배열에 색인을 생성하는 데 사용됩니다.

이것은 배열에 키 / 값 쌍의 벡터를 저장하는 곳입니다. 배열에서 인덱스가 될 수있는 값의 집합은 일반적으로 유형이 가질 수있는 모든 가능한 값의 수보다 작기 때문에 해시가 발생할 수 있습니다 알고리즘은 두 개의 개별 키에 대해 동일한 값을 생성합니다. 좋은 해시 알고리즘 (이것은 일반적인 해시 알고리즘 가능성이 알 수없는 특정 정보를 가지고 있기 때문에 일반적으로 유형에 이관되는 이유입니다) 가능한 한이 문제를 방지 할 수 있지만 방지하기 위해 불가능합니다.

이 때문에 동일한 해시 코드를 생성하는 여러 키를 가질 수 있습니다. 이 경우 벡터의 항목이 반복되고 벡터의 키와 조회중인 키가 직접 비교됩니다. 발견되면 키가 크고 키와 연관된 값이 리턴되고, 그렇지 않으면 아무것도 리턴되지 않습니다.


10

당신은 많은 것들과 배열을 취합니다.

각각의 경우 해시라는 인덱스를 구성합니다. 해시의 중요한 점은 해시가 많이 흩어져 있다는 것입니다. 비슷한 두 가지가 비슷한 해시를 갖기를 원하지 않습니다.

해시로 표시된 위치에 배열에 물건을 넣습니다. 주어진 해시에서 둘 이상의 항목이 종료 될 수 있으므로, 일반적으로 버킷이라고하는 적절한 배열 또는 다른 항목으로 항목을 저장합니다.

해시에서 물건을 찾을 때 동일한 단계를 거쳐 해시 값을 파악한 다음 해당 위치의 버킷에 무엇이 있는지 찾고 원하는 것이 있는지 확인합니다.

해싱이 잘 작동하고 배열이 충분히 크면 배열의 특정 인덱스에 최대 몇 가지만 있으므로 크게 살펴볼 필요가 없습니다.

보너스 포인트의 경우 해시 테이블에 액세스 할 때 찾은 항목 (있는 경우)이 버킷의 시작 부분으로 이동하므로 다음에 가장 먼저 확인해야합니다.


1
마지막으로 다른 사람들이 언급하지 않은 점에 감사드립니다
Sandeep Raju Prabhakar

4

지금까지의 모든 대답은 훌륭하며 해시 테이블의 작동 방식에 대한 다양한 측면을 살펴보십시오. 다음은 도움이 될만한 간단한 예입니다. 소문자 알파벳 문자열이있는 일부 항목을 키로 저장한다고 가정 해 보겠습니다.

simon이 설명했듯이 해시 함수는 넓은 공간에서 작은 공간으로 매핑하는 데 사용됩니다. 이 예제에서 해시 함수를 간단하고 순진하게 구현하면 문자열의 첫 문자를 가져 와서 정수로 매핑 할 수 있으므로 "alligator"에는 해시 코드가 0이고 "bee"에는 해시 코드가 1, " 얼룩말 "25 등입니다

다음에는 26 개의 버킷 (Java의 ArrayList 일 수 있음)이 배열되어 있으며 키의 해시 코드와 일치하는 항목을 버킷에 넣습니다. 동일한 문자로 시작하는 키가있는 항목이 두 개 이상인 경우 동일한 해시 코드를 가지므로 해당 해시 코드의 버킷에 모두 들어가므로 버킷에서 선형 검색을 수행해야합니다. 특정 항목을 찾으십시오.

이 예에서 알파벳에 걸쳐 키가있는 수십 개의 항목이 있다면 매우 잘 작동합니다. 그러나 백만 개의 항목이나 모든 키가 모두 'a'또는 'b'로 시작하면 해시 테이블이 이상적이지 않습니다. 더 나은 성능을 얻으려면 다른 해시 함수 및 / 또는 더 많은 버킷이 필요합니다.


3

그것을 보는 또 다른 방법이 있습니다.

배열 A의 개념을 이해한다고 가정합니다. 이는 A의 크기에 관계없이 한 번에 I 번째 요소 A [I]에 도달 할 수있는 인덱싱 작업을 지원하는 것입니다.

예를 들어, 연령대가 모두 다른 사람들 그룹에 대한 정보를 저장하려는 경우 간단한 방법은 충분히 큰 배열을 사용하고 각 사람의 나이를 배열의 색인으로 사용하는 것입니다. 이런 식으로, 당신은 모든 사람의 정보에 한 단계 접근 할 수 있습니다.

물론 나이가 같은 사람이 두 명 이상있을 수 있으므로 각 항목에 배열에 넣는 것은 해당 연령의 모든 사람들의 목록입니다. 따라서 한 번에 개인의 정보를 얻을 수 있고 해당 목록에서 약간의 검색 ( "버킷"이라고 함)을 얻을 수 있습니다. 양동이가 커질 사람들이 너무 많을 때만 속도가 느려집니다. 그런 다음 더 큰 배열이 필요하고 나이를 사용하는 대신 성의 처음 몇 글자와 같이 사람에 대해 더 많은 식별 정보를 얻는 다른 방법이 필요합니다.

이것이 기본 아이디어입니다. 나이를 사용하는 대신 가치를 널리 퍼뜨리는 사람의 기능을 사용할 수 있습니다. 이것이 해시 함수입니다. 사람 이름의 ASCII 표현의 세 번째 비트마다 순서대로 스크램블이 가능합니다. 중요한 것은 속도가 작은 버킷에 달려 있기 때문에 너무 많은 사람들이 같은 버킷에 해시하는 것을 원하지 않는다는 것입니다.


2

해시 계산 방법은 일반적으로 해시 테이블이 아니라 추가 된 항목에 따라 다릅니다. .net 및 Java와 같은 프레임 워크 / 기본 클래스 라이브러리에서 각 오브젝트에는이 오브젝트의 해시 코드를 리턴하는 GetHashCode () (또는 유사한) 메소드가 있습니다. 이상적인 해시 코드 알고리즘과 정확한 구현은 객체에서 나타내는 데이터에 따라 다릅니다.


2

해시 테이블은 실제 계산이 랜덤 액세스 머신 모델을 따른다는 사실에 전적으로 작동합니다. 즉, 메모리의 모든 주소의 값은 O (1) 시간 또는 일정한 시간에 액세스 할 수 있습니다.

따라서 키 유니버스 (응용 프로그램에 사용할 수있는 모든 가능한 키 세트 (예 : 학생의 롤 번호, 4 자리 인 경우이 유니버스는 1에서 9999까지의 숫자 집합))가 있고 시스템에 메모리를 할당 할 수있는 유한 한 크기의 크기로 매핑하는 방법, 이론적으로 해시 테이블이 준비되었습니다.

일반적으로 응용 프로그램에서 키의 유니버스 크기는 해시 테이블에 추가하려는 요소 수보다 매우 큽니다 (32GB이므로 정수 값을 해시하는 데 1GB 메모리를 낭비하고 싶지 않습니다. 이진 reprsentaion에서 조금 길다). 따라서이 해싱을 사용합니다. 일종의 "수학적"연산 혼합 방식으로, 큰 우주를 메모리에 수용 할 수있는 작은 값 집합에 매핑합니다. 실제로 해시 테이블의 공간은 종종 (요소 수 * 각 요소의 크기)와 동일한 "순서"(big-O)이므로 많은 메모리를 낭비하지 않습니다.

이제 큰 집합이 작은 집합에 매핑되면 매핑은 다 대일이어야합니다. 따라서 다른 키에는 동일한 공간이 할당됩니다 (?? 불공평). 이것을 처리하는 몇 가지 방법이 있습니다. 나는 그중 두 가지를 알고 있습니다.

  • 연결된 목록에 대한 참조로 값에 할당 된 공간을 사용하십시오. 이 링크 된 목록은 하나 이상의 값을 저장하며,이 값은 다 대일 매핑의 동일한 슬롯에 있습니다. 링크 된 목록에는 검색을하는 사람을 돕는 키도 포함되어 있습니다. 같은 아파트에있는 많은 사람들과 비슷합니다. 배달원이 오면 그는 방으로 가서 그 사람을 구체적으로 요구합니다.
  • 단일 값이 아닌 매번 동일한 값 시퀀스를 제공하는 배열에 이중 해시 함수를 사용하십시오. 값을 저장하려고하면 필요한 메모리 위치가 사용 가능한지 또는 사용 중인지 확인합니다. 그것이 무료라면, 거기에 내 가치를 저장할 수 있고, 그것이 점유되면 나는 시퀀스에서 다음 값을 가져 와서 무료 위치를 찾을 때까지 값을 저장합니다. 값을 검색하거나 검색 할 때 시퀀스에서 지정한 것과 동일한 경로로 돌아가서 각 위치에서 값을 찾거나 배열에서 가능한 모든 위치를 검색 할 때까지 값이 있는지 묻습니다.

CLRS의 알고리즘 소개는이 주제에 대한 아주 좋은 통찰력을 제공합니다.


0

프로그래밍 어휘를 찾는 모든 사람들에게 다음은 작동 방식입니다. 고급 해시 테이블의 내부 구현에는 스토리지 할당 / 할당 및 검색에 대한 많은 복잡성과 최적화가 있지만 최상위 아이디어는 거의 동일합니다.

(void) addValue : (object) value
{
   int bucket = calculate_bucket_from_val(value);
   if (bucket) 
   {
       //do nothing, just overwrite
   }
   else   //create bucket
   {
      create_extra_space_for_bucket();
   }
   put_value_into_bucket(bucket,value);
}

(bool) exists : (object) value
{
   int bucket = calculate_bucket_from_val(value);
   return bucket;
}

여기서 calculate_bucket_from_val()모든 고유성 마법이 발생해야하는 해시 함수입니다.

일반적으로 주어진 값을 삽입하려면 버킷이 저장해야 할 값에서 고유하고 파생 가능해야합니다.

버킷은 값이 저장되는 공간입니다. 여기서는 배열 인덱스로 int를 유지했지만 메모리 위치 일 수도 있습니다.


1
"엄격한 규칙 : 주어진 값을 삽입하려면 버킷이 저장해야하는 값에서 고유하고 파생 가능해야합니다." -이것은 완벽한 해시 함수를 설명하며 , 일반적으로 컴파일 타임에 알려진 수백 또는 수천 개의 값에 대해서만 가능합니다. 대부분의 해시 테이블은 충돌 을 처리해야합니다 . 또한 해시 테이블은 비어 있는지 여부에 관계없이 모든 버킷에 공간을 할당하는 반면 의사 코드 create_extra_space_for_bucket()는 새 키를 삽입 하는 동안 단계를 문서화 합니다. 버킷은 포인터 일 수 있습니다.
Tony Delroy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.