해시 테이블의 작동 방식에 대한 설명을 찾고 있습니다.
예를 들어 키를 가져 와서 해시를 계산하고 (어떻게 설명을 찾고 있는지) 값을 저장하는 배열의 위치를 알아 내기 위해 모듈로를 수행하지만 지식이 멈 춥니 다. .
누구나 프로세스를 명확히 할 수 있습니까?
편집 : 해시 코드 계산 방법에 대해 구체적으로 묻지 않고 해시 테이블 작동 방법에 대한 일반적인 개요를 제공합니다.
해시 테이블의 작동 방식에 대한 설명을 찾고 있습니다.
예를 들어 키를 가져 와서 해시를 계산하고 (어떻게 설명을 찾고 있는지) 값을 저장하는 배열의 위치를 알아 내기 위해 모듈로를 수행하지만 지식이 멈 춥니 다. .
누구나 프로세스를 명확히 할 수 있습니까?
편집 : 해시 코드 계산 방법에 대해 구체적으로 묻지 않고 해시 테이블 작동 방법에 대한 일반적인 개요를 제공합니다.
답변:
평신도의 용어로 설명합니다.
도서관에 책을 채우고 책을 채우는 것이 아니라 필요할 때 쉽게 다시 찾을 수 있기를 원한다고 가정 해 봅시다.
따라서 책을 읽고 싶은 사람이 책의 제목과 부팅 할 정확한 제목을 알고 있으면 그게 전부라고 결정합니다. 제목을 통해 사서의 도움을 받아 그 사람이 책을 쉽고 빠르게 찾을 수 있어야합니다.
어떻게 그렇게 할 수 있습니까? 글쎄, 당신은 분명히 당신이 각 책을 넣는 곳의 목록을 유지할 수 있지만, 도서관을 검색하는 것과 같은 문제가 있습니다. 당신은 목록을 검색해야합니다. 물론, 목록은 더 작고 쉽게 검색 할 수 있지만 여전히 라이브러리 (또는 목록)의 한쪽 끝에서 다른 쪽 끝까지 순차적으로 검색하고 싶지는 않습니다.
책 제목으로 한 번에 올바른 지점을 제공 할 수있는 것을 원하므로 필요한 선반으로 걸어 가서 책을 집어 올리면됩니다.
그러나 어떻게 할 수 있습니까? 글쎄, 라이브러리를 채울 때 약간의 예측과 라이브러리를 채울 때 많은 작업이 있습니다.
라이브러리를 한쪽 끝에서 다른 쪽 끝까지 채우는 대신 영리한 작은 방법을 고안하십시오. 책 제목을 가져 와서 작은 컴퓨터 프로그램을 통해 실행하십시오.이 프로그램은 선반 번호와 해당 선반의 슬롯 번호를 표시합니다. 여기가 책을 놓는 곳입니다.
이 프로그램의 장점은 나중에 사람이 책을 읽기 위해 다시 들어올 때 프로그램을 통해 제목을 다시 한 번 피드하고 원래 제공된 선반 번호와 슬롯 번호를 다시 얻는다는 것입니다. 책이있는 곳.
다른 사람들이 이미 언급 했듯이이 프로그램은 해시 알고리즘 또는 해시 계산이라고하며 일반적으로 공급 된 데이터 (이 경우 책 제목)를 가져 와서 숫자를 계산하여 작동합니다.
간단히하기 위해 각 문자와 기호를 숫자로 변환하고 모두 합산한다고 가정 해 봅시다. 실제로는 그보다 훨씬 복잡하지만 지금은 그대로 두겠습니다.
이러한 알고리즘의 장점은 동일한 입력을 반복해서 입력하면 매번 같은 숫자를 뱉어내는 것입니다.
기본적으로 해시 테이블이 작동하는 방식입니다.
기술적 인 내용은 다음과 같습니다.
첫째, 숫자의 크기가 있습니다. 일반적으로 이러한 해시 알고리즘의 출력은 일반적으로 테이블에있는 공간보다 훨씬 큰 일부 범위 내에 있습니다. 예를 들어, 도서관에 정확히 백만 권의 책을 넣을 공간이 있다고 가정 해 봅시다. 해시 계산의 출력은 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에 넣습니다.
이것은 다음 문제로 이어집니다. 충돌. 알고리즘은 책을 간격을 두어 라이브러리를 정확하게 채울 수있는 방법이 없기 때문에 (또는 원하는 경우 해시 테이블) 항상 이전에 사용 된 숫자를 계산하게됩니다. 도서관의 의미에서 책장에 넣고 싶은 선반 번호와 슬롯 번호에 도달하면 이미 책이 있습니다.
데이터를 또 다른 계산으로 실행하여 테이블의 다른 지점을 가져 오거나 ( 이중 해싱 ) 주어진 공간에 가까운 공간을 찾는 것 (예 : 슬롯을 가정 한 이전 책 바로 옆)을 포함한 다양한 충돌 처리 방법이 있습니다. 선형 프로빙 이라고도 함 ). 이것은 나중에 책을 찾으려고 할 때 약간의 파고가 있다는 것을 의미하지만, 도서관의 한쪽 끝에서 시작하는 것보다 여전히 낫습니다.
마지막으로 라이브러리가 허용하는 것보다 많은 책을 라이브러리에 넣을 수 있습니다. 다시 말해, 더 큰 라이브러리를 만들어야합니다. 라이브러리의 정확한 지점은 라이브러리의 정확한 현재 크기를 사용하여 계산되었으므로 라이브러리의 크기를 조정하면 계산을 수행 한 후 해당 지점을 찾기 위해 모든 책에 대해 새로운 지점을 찾아야 할 수도 있습니다. 변경되었습니다.
이 설명이 버킷과 함수보다 지구에 약간 더 가깝기를 바랍니다. :)
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 버킷이 "폐기"됩니까? 킨다. 사용 된 총 메모리가 동일합니다.
사용법 및 Lingo :
실제 예 :
1803 년에 설립되었으며 컴퓨터 기술이 부족한 Hash & Co. 는 약 30,000 명의 고객에 대한 상세 정보 (기록)를 보관하기 위해 총 300 개의 파일 캐비닛을 보유하고있었습니다. 각 파일 폴더는 클라이언트 번호 (0-29,999의 고유 번호)로 명확하게 식별되었습니다.
당시의 사무원은 근무하는 직원의 고객 기록을 신속하게 가져 와서 저장해야했습니다. 직원들은 해싱 방법을 사용하여 레코드를 저장하고 검색하는 것이 더 효율적이라고 결정했습니다.
고객 기록을 제출하기 위해 사무원은 폴더에 기록 된 고유 고객 번호를 사용합니다. 이 클라이언트 번호를 사용하면 포함 된 파일 캐비닛을 식별하기 위해 해시 키 를 300 씩 변조합니다 . 파일 캐비닛을 열면 클라이언트 번호별로 많은 폴더가 포함되어 있음을 알 수 있습니다. 올바른 위치를 식별 한 후에는 간단히 해당 위치로 이동합니다.
고객 기록을 검색하기 위해 서류 제출시 사무원에게 고객 번호가 부여됩니다. 이 고유 한 클라이언트 번호 ( 해시 키 )를 사용하여 클라이언트 폴더가있는 파일 캐비넷을 판별하기 위해 300으로 변조합니다. 그들이 파일 캐비닛을 열었을 때 클라이언트 번호별로 많은 폴더가 포함되어 있음을 발견했습니다. 레코드를 검색하면 클라이언트 폴더를 빠르게 찾아서 검색합니다.
실제 예에서 버킷 은 파일 캐비닛 이며 레코드 는 파일 폴더 입니다.
기억해야 할 중요한 점은 컴퓨터 (및 해당 알고리즘)가 문자열보다 숫자를 더 잘 처리한다는 것입니다. 따라서 인덱스를 사용하여 큰 배열에 액세스하는 것이 순차적으로 액세스하는 것보다 훨씬 빠릅니다.
Simon이 내가 매우 중요하다고 생각한 것처럼 해싱 부분은 큰 공간 (임의의 길이, 일반적으로 문자열 등)을 변환하고 색인을 위해 작은 크기의 공간 (일반적으로 숫자, 알려진 숫자)으로 매핑하는 것입니다. 기억해야 할 것이 매우 중요합니다!
따라서 위의 예에서 30,000 개의 가능한 클라이언트가 더 작은 공간에 매핑되었습니다.
이것의 주요 아이디어는 일반적으로 시간이 많이 걸리는 실제 검색 속도를 높이기 위해 전체 데이터 세트를 세그먼트로 나누는 것입니다. 위의 예에서 300 개의 파일 캐비닛 각각은 (통계적으로) 약 100 개의 레코드를 포함합니다. 100 개의 레코드를 통해 (주문에 관계없이) 검색하는 것이 30,000을 처리하는 것보다 훨씬 빠릅니다.
일부는 실제로 이미이 작업을 수행 한 것을 알 수 있습니다. 그러나 해시 키를 생성하기 위해 해싱 방법을 고안하는 대신 대부분의 경우 단순히 성의 첫 글자를 사용합니다. 따라서 각각 A부터 Z까지의 문자를 포함하는 26 개의 파일 캐비닛이있는 경우 이론적으로 데이터를 세그먼트 화하고 파일 정리 및 검색 프로세스를 향상 시켰습니다.
도움이 되었기를 바랍니다,
복숭아!
100
레코드 (30k 레코드 / 300 캐비닛 = 100)를 포함합니다. 편집 할 가치가 있습니다.
TonyD
하여 텍스트 필드에 입력 한 SHA-1 해시를 생성하십시오 . 당신은 다음과 같은 무언가의 생성 된 값으로 끝날 것입니다 e5dc41578f88877b333c8b31634cf77e4911ed8c
. 이것은 16 진수 160 비트 (20 바이트)에 지나지 않습니다. 그런 다음이를 사용하여 레코드를 저장하는 데 사용할 버킷 (한정 수량)을 결정할 수 있습니다.
이것은 이론의 매우 깊은 영역으로 밝혀졌지만 기본 개요는 간단합니다.
본질적으로 해시 함수는 하나의 공백 (임의의 길이의 문자열)에서 물건을 가져 와서 인덱싱에 유용한 공간 (예 : 부호없는 정수)에 매핑하는 함수 일뿐입니다.
해시해야 할 공간이 적다면 정수로 해석하는 것만으로도 벗어날 수 있습니다 (예 : 4 바이트 문자열)
그러나 일반적으로 더 큰 공간이 있습니다. 키로 허용하는 것의 공간이 uint32 또는 기타 색인을 생성하는 데 사용하는 것의 공간보다 크면 각각에 대해 고유 한 값을 가질 수 없습니다. 둘 이상의 것들이 동일한 결과로 해시되면 중복을 적절한 방식으로 처리해야합니다 (일반적으로 충돌이라고하며 처리 방법 또는 처리하지 않는 방식은 현재 상태에 따라 조금씩 다름) 해시 사용).
이것은 동일한 결과를 얻지 않기를 원하며 해시 함수가 빠르기를 원할 수도 있습니다.
이 두 속성 (및 다른 속성)의 균형을 유지하면 많은 사람들이 바쁘게 지 냈습니다!
실제로는 일반적으로 응용 프로그램에 잘 작동하는 것으로 알려진 기능을 찾아서 사용할 수 있어야합니다.
이제이 작업을 해시 테이블로 만들려면 메모리 사용에 신경 쓰지 않았다고 상상해보십시오. 그런 다음 인덱싱 세트 (예 : 모든 uint32)만큼 배열을 만들 수 있습니다. 테이블에 무언가를 추가하면 키를 해시하고 해당 인덱스의 배열을 봅니다. 거기에 아무것도 없다면, 거기에 가치를 두십시오. 이미 무언가가 있다면,이 새로운 항목을 해당 주소에있는 것들의 목록에 추가하여 어떤 항목이 실제로 어떤 키에 속하는지를 찾기에 충분한 정보 (원래 키 또는 영리한 것)와 함께 추가하십시오.
오래 갈수록 해시 테이블 (배열)의 모든 항목이 비어 있거나 하나의 항목 또는 항목 목록을 포함합니다. 검색은 배열에 색인을 생성하고 값을 반환하거나 값 목록을 걷고 올바른 값을 반환하는 것처럼 간단합니다.
물론 실제로는 이렇게 할 수 없으며 너무 많은 메모리를 낭비합니다. 따라서 희소 배열을 기반으로 모든 작업을 수행합니다 (실제로 사용되는 항목은 다른 항목은 암시 적으로 null입니다).
이 작업을 개선하기위한 많은 구성표와 요령이 있지만 이것이 기본입니다.
int
: 1-in-1000 sparseness 및 4k page의 키 = 대부분의 페이지 터치) OS는 주소 공간이 충분할 때 모든 0 페이지를 효율적으로 처리하므로 (사용하지 않는 모든 버킷 페이지는 백업 메모리가 필요하지 않습니다.)
많은 답변이 있지만 그중 어느 것도 시각적이지 않으며 해시 테이블은 시각화 할 때 쉽게 "클릭"할 수 있습니다.
해시 테이블은 종종 연결된 목록의 배열로 구현됩니다. 사람들의 이름을 저장하는 테이블을 상상하면 몇 번의 삽입 후에 아래와 같이 메모리에 배치 될 수 있습니다. 여기서 ()
-닫힌 숫자는 텍스트 / 이름의 해시 값입니다.
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에서의 %)에 의해, 랜덤 매핑에 의해 달성되는 것보다 충돌이 적고 충돌 요소의 링크 된리스트가 적어진다. 또한 강력한 해시를 생성하는 데 걸리는 시간을 절약하고 키를 순서대로 조회하면 메모리 근처의 버킷에서 발견되어 캐시 적중률이 향상됩니다. 키 가 잘 증가 하지 않으면 무작위로 배치되어 버킷에 배치를 완전히 무작위 화하기 위해 강력한 해시 기능이 필요하지 않을 것이라는 희망이 있습니다.
여러분은 이것을 완전히 설명하는 데 매우 가깝지만 몇 가지 사항이 누락되었습니다. 해시 테이블은 단지 배열입니다. 배열 자체는 각 슬롯에 무언가를 포함합니다. 최소한이 슬롯에 해시 값 또는 값 자체를 저장합니다. 이 외에도이 슬롯에 충돌 한 연결 / 체인 값 목록을 저장하거나 공개 주소 지정 방법을 사용할 수 있습니다. 이 슬롯에서 검색하려는 다른 데이터에 대한 포인터를 저장할 수도 있습니다.
해시 값 자체는 일반적으로 값을 배치 할 슬롯을 나타내지 않습니다. 예를 들어, 해시 값은 음의 정수 값일 수 있습니다. 분명히 음수는 배열 위치를 가리킬 수 없습니다. 또한 해시 값은 사용 가능한 슬롯보다 여러 번 큰 경향이 있습니다. 따라서 값을 입력해야하는 슬롯을 파악하기 위해 해시 테이블 자체에서 다른 계산을 수행해야합니다. 이것은 다음과 같은 계수 수학 연산으로 수행됩니다.
uint slotIndex = hashValue % hashTableSize;
이 값은 값이 들어갈 슬롯입니다. 열린 주소 지정에서 슬롯에 다른 해시 값 및 / 또는 다른 데이터가 이미 채워져 있으면 다음 슬롯을 찾기 위해 계수 작업이 다시 한 번 실행됩니다.
slotIndex = (remainder + 1) % hashTableSize;
슬롯 인덱스를 결정하는 다른 고급 방법이있을 수 있지만 이것이 내가 본 일반적인 방법입니다 ... 더 나은 성능을 가진 다른 사람들에게 관심이 있습니다.
모듈러스 방법을 사용하면 크기가 1000 인 테이블이 있으면 1과 1000 사이의 해시 값이 해당 슬롯으로 이동합니다. 음수 값과 1000보다 큰 값은 잠재적으로 충돌하는 슬롯 값입니다. 이러한 일이 발생할 가능성은 해시 방법과 해시 테이블에 추가하는 총 항목 수에 따라 다릅니다. 일반적으로 해시 테이블의 크기를 추가하여 총 값 수는 크기의 약 70 %에 불과하도록하는 것이 가장 좋습니다. 해시 함수가 균일 한 분산 작업을 수행하는 경우 일반적으로 버킷 / 슬롯 충돌이 거의 발생하지 않으며 조회 및 쓰기 작업 모두에서 매우 빠르게 수행됩니다. 더할 총 값의 수를 미리 알 수없는 경우, 어떤 방법 으로든 추측 해보십시오.
이것이 도움이 되었기를 바랍니다.
PS-C #에서이 GetHashCode()
방법은 속도가 느리고 테스트 한 많은 조건에서 실제 값 충돌이 발생합니다. 정말 재미있게, 자신의 해시 함수를 작성하고 해시하는 특정 데이터에 충돌하지 않도록 해보십시오. GetHashCode보다 빠르게 실행되며 상당히 균일하게 배포됩니다. int 크기의 해시 코드 값 대신 long을 사용 하여이 작업을 수행했으며 충돌이없는 해시 테이블의 최대 3,200 만 개의 전체 해시 값에서 꽤 잘 작동했습니다. 불행히도 코드가 고용주에게 속한 코드를 공유 할 수는 없지만 특정 데이터 도메인에서 코드가 가능하다는 것을 알 수 있습니다. 이것을 달성 할 수 있으면 해시 테이블은 매우 빠릅니다. :)
remainder
는 원래의 모듈로 계산 결과를 나타내며 사용 가능한 다음 슬롯을 찾기 위해 1을 더합니다.
long
해시 값에 대한 이야기는 그것이 달성 한 것을 암시합니다), mod / % 작업이 아닌 후에 해시 테이블에서 충돌하지 않도록 하십시오 (일반적인 경우) ).
이것이 내 이해에서 작동하는 방식입니다.
다음은 예입니다. 전체 테이블을 일련의 버킷으로 묘사하십시오. 영숫자 해시 코드로 구현하고 알파벳의 각 문자에 대해 하나의 버킷이 있다고 가정하십시오. 이 구현은 해시 코드가있는 각 항목을 해당 버킷에 특정 문자로 시작합니다.
200 개의 객체가 있지만 그 중 15 개만 문자 'B'로 시작하는 해시 코드를 가지고 있다고 가정 해 봅시다. 해시 테이블은 200 개의 객체가 아니라 'B'버킷에서 15 개의 객체 만 검색하고 검색하면됩니다.
해시 코드를 계산하는 한 마법은 없습니다. 목표는 다른 객체가 다른 코드를 반환하고 동일한 객체가 동일한 코드를 반환하도록하는 것입니다. 모든 인스턴스에 대해 항상 해시 코드와 동일한 정수를 반환하는 클래스를 작성할 수 있지만 하나의 거대한 버킷이되기 때문에 해시 테이블의 유용성을 본질적으로 파괴합니다.
짧고 달다:
해시 테이블은 배열을 마무리하고 호출 할 수 있습니다 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 %에 도달하면 내부 배열의 크기를 조정하고 모든 항목을 기존 배열에서 새 배열로 옮길 수 있습니다.
그것보다 훨씬 간단합니다.
해시 테이블은 키 / 값 쌍을 포함하는 벡터 의 배열 (일반적으로 희소 한 것)에 지나지 않습니다 . 이 배열의 최대 크기는 일반적으로 해시 테이블에 저장되는 데이터 유형에 대해 가능한 값 세트의 항목 수보다 작습니다.
해시 알고리즘은 배열에 저장 될 항목의 값을 기반으로 해당 배열에 색인을 생성하는 데 사용됩니다.
이것은 배열에 키 / 값 쌍의 벡터를 저장하는 곳입니다. 배열에서 인덱스가 될 수있는 값의 집합은 일반적으로 유형이 가질 수있는 모든 가능한 값의 수보다 작기 때문에 해시가 발생할 수 있습니다 알고리즘은 두 개의 개별 키에 대해 동일한 값을 생성합니다. 좋은 해시 알고리즘 (이것은 일반적인 해시 알고리즘 가능성이 알 수없는 특정 정보를 가지고 있기 때문에 일반적으로 유형에 이관되는 이유입니다) 가능한 한이 문제를 방지 할 수 있지만 방지하기 위해 불가능합니다.
이 때문에 동일한 해시 코드를 생성하는 여러 키를 가질 수 있습니다. 이 경우 벡터의 항목이 반복되고 벡터의 키와 조회중인 키가 직접 비교됩니다. 발견되면 키가 크고 키와 연관된 값이 리턴되고, 그렇지 않으면 아무것도 리턴되지 않습니다.
당신은 많은 것들과 배열을 취합니다.
각각의 경우 해시라는 인덱스를 구성합니다. 해시의 중요한 점은 해시가 많이 흩어져 있다는 것입니다. 비슷한 두 가지가 비슷한 해시를 갖기를 원하지 않습니다.
해시로 표시된 위치에 배열에 물건을 넣습니다. 주어진 해시에서 둘 이상의 항목이 종료 될 수 있으므로, 일반적으로 버킷이라고하는 적절한 배열 또는 다른 항목으로 항목을 저장합니다.
해시에서 물건을 찾을 때 동일한 단계를 거쳐 해시 값을 파악한 다음 해당 위치의 버킷에 무엇이 있는지 찾고 원하는 것이 있는지 확인합니다.
해싱이 잘 작동하고 배열이 충분히 크면 배열의 특정 인덱스에 최대 몇 가지만 있으므로 크게 살펴볼 필요가 없습니다.
보너스 포인트의 경우 해시 테이블에 액세스 할 때 찾은 항목 (있는 경우)이 버킷의 시작 부분으로 이동하므로 다음에 가장 먼저 확인해야합니다.
지금까지의 모든 대답은 훌륭하며 해시 테이블의 작동 방식에 대한 다양한 측면을 살펴보십시오. 다음은 도움이 될만한 간단한 예입니다. 소문자 알파벳 문자열이있는 일부 항목을 키로 저장한다고 가정 해 보겠습니다.
simon이 설명했듯이 해시 함수는 넓은 공간에서 작은 공간으로 매핑하는 데 사용됩니다. 이 예제에서 해시 함수를 간단하고 순진하게 구현하면 문자열의 첫 문자를 가져 와서 정수로 매핑 할 수 있으므로 "alligator"에는 해시 코드가 0이고 "bee"에는 해시 코드가 1, " 얼룩말 "25 등입니다
다음에는 26 개의 버킷 (Java의 ArrayList 일 수 있음)이 배열되어 있으며 키의 해시 코드와 일치하는 항목을 버킷에 넣습니다. 동일한 문자로 시작하는 키가있는 항목이 두 개 이상인 경우 동일한 해시 코드를 가지므로 해당 해시 코드의 버킷에 모두 들어가므로 버킷에서 선형 검색을 수행해야합니다. 특정 항목을 찾으십시오.
이 예에서 알파벳에 걸쳐 키가있는 수십 개의 항목이 있다면 매우 잘 작동합니다. 그러나 백만 개의 항목이나 모든 키가 모두 'a'또는 'b'로 시작하면 해시 테이블이 이상적이지 않습니다. 더 나은 성능을 얻으려면 다른 해시 함수 및 / 또는 더 많은 버킷이 필요합니다.
그것을 보는 또 다른 방법이 있습니다.
배열 A의 개념을 이해한다고 가정합니다. 이는 A의 크기에 관계없이 한 번에 I 번째 요소 A [I]에 도달 할 수있는 인덱싱 작업을 지원하는 것입니다.
예를 들어, 연령대가 모두 다른 사람들 그룹에 대한 정보를 저장하려는 경우 간단한 방법은 충분히 큰 배열을 사용하고 각 사람의 나이를 배열의 색인으로 사용하는 것입니다. 이런 식으로, 당신은 모든 사람의 정보에 한 단계 접근 할 수 있습니다.
물론 나이가 같은 사람이 두 명 이상있을 수 있으므로 각 항목에 배열에 넣는 것은 해당 연령의 모든 사람들의 목록입니다. 따라서 한 번에 개인의 정보를 얻을 수 있고 해당 목록에서 약간의 검색 ( "버킷"이라고 함)을 얻을 수 있습니다. 양동이가 커질 사람들이 너무 많을 때만 속도가 느려집니다. 그런 다음 더 큰 배열이 필요하고 나이를 사용하는 대신 성의 처음 몇 글자와 같이 사람에 대해 더 많은 식별 정보를 얻는 다른 방법이 필요합니다.
이것이 기본 아이디어입니다. 나이를 사용하는 대신 가치를 널리 퍼뜨리는 사람의 기능을 사용할 수 있습니다. 이것이 해시 함수입니다. 사람 이름의 ASCII 표현의 세 번째 비트마다 순서대로 스크램블이 가능합니다. 중요한 것은 속도가 작은 버킷에 달려 있기 때문에 너무 많은 사람들이 같은 버킷에 해시하는 것을 원하지 않는다는 것입니다.
해시 테이블은 실제 계산이 랜덤 액세스 머신 모델을 따른다는 사실에 전적으로 작동합니다. 즉, 메모리의 모든 주소의 값은 O (1) 시간 또는 일정한 시간에 액세스 할 수 있습니다.
따라서 키 유니버스 (응용 프로그램에 사용할 수있는 모든 가능한 키 세트 (예 : 학생의 롤 번호, 4 자리 인 경우이 유니버스는 1에서 9999까지의 숫자 집합))가 있고 시스템에 메모리를 할당 할 수있는 유한 한 크기의 크기로 매핑하는 방법, 이론적으로 해시 테이블이 준비되었습니다.
일반적으로 응용 프로그램에서 키의 유니버스 크기는 해시 테이블에 추가하려는 요소 수보다 매우 큽니다 (32GB이므로 정수 값을 해시하는 데 1GB 메모리를 낭비하고 싶지 않습니다. 이진 reprsentaion에서 조금 길다). 따라서이 해싱을 사용합니다. 일종의 "수학적"연산 혼합 방식으로, 큰 우주를 메모리에 수용 할 수있는 작은 값 집합에 매핑합니다. 실제로 해시 테이블의 공간은 종종 (요소 수 * 각 요소의 크기)와 동일한 "순서"(big-O)이므로 많은 메모리를 낭비하지 않습니다.
이제 큰 집합이 작은 집합에 매핑되면 매핑은 다 대일이어야합니다. 따라서 다른 키에는 동일한 공간이 할당됩니다 (?? 불공평). 이것을 처리하는 몇 가지 방법이 있습니다. 나는 그중 두 가지를 알고 있습니다.
CLRS의 알고리즘 소개는이 주제에 대한 아주 좋은 통찰력을 제공합니다.
프로그래밍 어휘를 찾는 모든 사람들에게 다음은 작동 방식입니다. 고급 해시 테이블의 내부 구현에는 스토리지 할당 / 할당 및 검색에 대한 많은 복잡성과 최적화가 있지만 최상위 아이디어는 거의 동일합니다.
(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를 유지했지만 메모리 위치 일 수도 있습니다.
create_extra_space_for_bucket()
는 새 키를 삽입 하는 동안 단계를 문서화 합니다. 버킷은 포인터 일 수 있습니다.