체이닝 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
"""체이닝에서 찾을 때 보는 노드 수: 문서 예시 체인의 정확한 값과 모의실험.
실행: python3 50_hash-chaining_verify.py
문서: 50.체이닝 (성능 절, 끝까지 훑기와 일찍 멈추기, 카드 C2~C3)
세는 것은 체인에서 들여다본 노드 수다(버킷 칸 자체는 세지 않는다).
주장 (3~5는 실험으로 확인한 것이고 증명이 아니다)
1. 문서 예시의 체인(키 12개, b = 17): 체인 길이의 평균은 12/17, 있는 키를 찾을 때 본 노드 수의 합은 18(평균 1.5).
2. 있는 키를 찾을 때의 기댓값은 정확히 1 + (n - 1)/(2b)다(키가 버킷에 고르게 흩어진다고 볼 때).
n이 크면 문서의 식 1 + a/2가 된다. 모의실험 값과 2% 안에서 맞는다.
3. 체인 길이의 평균은 a다.
4. 없는 키를 찾을 때: 체인을 끝까지 훑으면 평균 a개를 본다(문서의 식 U_n ~ a).
5. 정렬된 체인에서 "찾는 키보다 큰 노드에서 멈춘다"로 세면 더 적다.
a = 3이면 약 2.18개로, a/2 + 1 - (1 - e^-a)/a와 2% 안에서 맞는다. a가 작으면(0.5) 둘의 차이는 거의 없다.
"""
import math
import random
def build(keys, b):
chains = [[] for _ in range(b)]
for k in keys:
chains[k % b].append(k)
for c in chains:
c.sort()
return chains
def nodes_success(chains, k):
return chains[k % len(chains)].index(k) + 1
def nodes_fail_full(chains, k):
"""정렬하지 않은 체인처럼 끝까지 훑는다."""
return len(chains[k % len(chains)])
def nodes_fail_sorted(chains, k):
"""정렬된 체인: 찾는 키보다 큰 노드를 만나면 그 노드까지만 보고 멈춘다."""
count = 0
for x in chains[k % len(chains)]:
count += 1
if x > k:
break
return count
def predicted_sorted(a):
return a / 2 + 1 - (1 - math.exp(-a)) / a
def simulate(b, alpha, trials, rng):
n = int(alpha * b)
universe = 10 ** 9
s = full = srt = length = 0.0
for _ in range(trials):
keys = rng.sample(range(universe), n)
chains = build(keys, b)
present = set(keys)
s += sum(nodes_success(chains, k) for k in keys) / n
length += sum(len(c) for c in chains) / b
misses = []
while len(misses) < 2000:
k = rng.randrange(universe)
if k not in present:
misses.append(k)
full += sum(nodes_fail_full(chains, k) for k in misses) / len(misses)
srt += sum(nodes_fail_sorted(chains, k) for k in misses) / len(misses)
return s / trials, full / trials, srt / trials, length / trials, n
if __name__ == "__main__":
# 1. 문서 예시의 체인
slide_keys = [6, 12, 34, 29, 28, 11, 23, 7, 0, 33, 30, 45]
chains = build(slide_keys, 17)
assert [c for c in chains if c] == [[0, 34], [6, 23], [7], [11, 28, 45], [12, 29], [30], [33]]
assert sum(nodes_success(chains, k) for k in slide_keys) == 18
assert sum(len(c) for c in chains) / 17 == 12 / 17
# 카드 C2: 이 표에서 없는 키를 찾을 때. 홈 버킷 17곳의 체인 길이 평균 = 12/17
assert sum(len(c) for c in chains) == 12
# 2~5. 모의실험
rng = random.Random(50)
b = 2003
out = {}
for alpha in (0.5, 1.0, 3.0):
s, full, srt, length, n = simulate(b, alpha, 20, rng)
out[alpha] = (s, full, srt)
exact_s = 1 + (n - 1) / (2 * b)
assert abs(s - exact_s) / exact_s < 0.02
assert abs(exact_s - (1 + alpha / 2)) < 0.01
assert abs(length - n / b) < 1e-9
assert abs(full - alpha) / alpha < 0.03
assert abs(srt - predicted_sorted(alpha)) / predicted_sorted(alpha) < 0.03
assert srt <= full
assert out[3.0][2] < 2.3 and out[3.0][1] > 2.9 # 2.18쯤 대 3쯤
assert out[0.5][1] - out[0.5][2] < 0.05 # a가 작으면 차이가 거의 없다
# 카드 C3: a = 2일 때 식의 값
assert 1 + 2 / 2 == 2.0
for alpha in (0.5, 1.0, 3.0):
s, full, srt = out[alpha]
print(f"a = {alpha}: 있는 키 {s:.2f} (식 {1 + alpha / 2:.2f}), 없는 키 끝까지 {full:.2f} (식 {alpha:.2f}), "
f"정렬된 체인에서 멈추기 {srt:.2f} (어림 {predicted_sorted(alpha):.2f})")
print("ALL CHECKS PASSED")