W tym rozdziale przedstawimy kolejne bardzo naturalne rozwiązanie problemu dynamicznego słownika oparte na haszowaniu. Liczby całkowite ze zbioru S reprezentowanego przez nasz słownik będą przechowywane bezpośrednio w tablicy T[0..m−1]. Rozmiar T będzie większy niż n=|S|, choć liniowy względem n. Zwykle w zastosowaniach przyjmuje się m=2n, do udowodnienia asymptotycznych oszacowań na czasy działania operacji wystarczy m=(1+ε)⋅n, natomiast my dla ustalenia uwagi przyjmiemy m=3n. Algorytmy operacji słownikowych będą korzystały z pojedynczej funkcji haszującej h:U→{0..m−1}.
Algorytmy poszczególnych operacji są następujące:
NULL
. Nie możemy jednak tak zostawić tablicy T, ponieważ w spójnym bloku niepustych komórek zaczynającym się od pozycji p+1 może znajdować się element (lub elementy) y taki, że h(y)≤p; wówczas wynik operacji lookup(y) byłby niepoprawny. W tym celu znajdujemy pierwszy taki element y=T[r], wstawiamy go w T[p], i czyścimy jego komórkę, tzn. T[r] := NULL
. Oczywiście tę operację powtarzamy tak długo, jak występuje opisana powyżej niepożądana sytuacja. Dla jasności poniżej podajemy pseudokod.procedure delete (x) begin p := h(x); while (T[p] <> x) and (T[p] <> NULL) p := p + 1; T[p] := NULL fill(p) end; procedure fill(p) begin r := p+1; while (T[r] <> NULL) and (h(T[r]) > p) r := r + 1; if (T[r] <> NULL) then begin T[p] := T[r]; T[r] := NULL; fill(r); end; end;
Poprawność opisanych algorytmów powinna być dość jasna (w zasadzie tylko w przypadku delete jest się nad czym zastanawiać).
Zauważmy, że opisane operacje (szczególnie lookup i insert) operują z reguły na spójnym bloku pamięci (chyba że, pechowo, algorytm wyszukiwania musi ,,przeskoczyć'' z końca tablicy na początek). Taki sposób dostępu do pamięci jest kluczowy dla praktycznej efektywności algorytmów, gdyż w komputerach o współczesnej architekturze z reguły wykonany zostanie odczyt pojedynczego bloku pamięci przy pierwszej operacji dostępu do tablicy T, natomiast wartości kolejnych komórek zostaną już odczytane z szybkiej pamięci typu cache. Z tego względu haszowanie liniowe jest rozwiązaniem najczęściej wybieranym w praktyce. W tym wykładzie dowiemy się, że odpowiednio wybierając funkcję haszującą możemy zagwarantować także teoretyczną efektywność wszystkich operacji słownikowych.
Pamiętamy z wcześniejszego wykładu, że przy haszowaniu przez łańcuchowanie, jeśli h jest wybrana losowo z rodziny uniwersalnej, to oczekiwane czasy operacji słownikowych są stałe. Od czasu uzyskania tego wyniku przez Cartera i Wegmana w 1977r. pozostawało otwartym pytanie, czy można uzyskać analogiczny wynik dla haszowania liniowego, używając rodziny uniwersalnej lub jakiejś innej rodziny funkcji haszujących. Dopiero w 2007 odpowiedzi udzielili Rasmus Pagh, Anna Pagh i Milan Ruzic.
Twierdzenie 1
Jeśli w haszowaniu liniowym h jest wybrana losowo z uniwersalnej rodziny funkcji haszujących Cartera i Wegmana (patrz Przykład 2 w I wykładzie o haszowaniu), to istnieje S⊂U taki, że całkowity oczekiwany czas wstawienia wszystkich elementów z S wynosi Ω(nlogn).
Równocześnie pokazali oni także następujący wynik pozytywny:
Twierdzenie 2
Jeśli w haszowaniu liniowym h jest wybrana losowo z dowolnej 5-niezależnej rodziny funkcji haszujących, to oczekiwany czas operacji słownikowych jest stały, a dokładniej wynosi O((1−α)−7/6), gdzie α=n/m.
Zacznijmy od tego, że wynik O((1−α)−7/6) jest naprawdę dobry, gdyż O((1−α)−1) to oczekiwany czas jaki dostajemy, gdy zamiast szukać wolnego miejsca na x używając kolejnych pozycji w tablicy T, korzystamy z w pełni losowej permutacji pozycji (patrz np. podręcznik Cormena i innych), co jest bardzo wyidealizowanym założeniem. Twierdzenie 2 jest dość zaskakujące, gdyż w świetle twierdzenia 1, które ,,z grubsza'' mówi, że 2-niezależność nie wystarcza, mogłoby się wydawać, że również O(1)-niezależność nie jest wystarczająca. Dodatkowo dziwi tu liczba 5, na pierwszy rzut oka wygląda to na niedokładne szacowanie, nieoptymalny wynik. Nic bardziej błędnego, gdyż w 2010r. Patrascu i Thorup wykazali, że
Twierdzenie 3
Istnieje 4-niezależna rodzina funkcji haszujących H taka, że jeśli w haszowaniu liniowym h jest wybrana losowo z H, to istnieje S⊂U taki, że całkowity oczekiwany czas wstawienia wszystkich elementów z S wynosi Ω(nlogn).
W dalszej części udowodnimy uproszczoną wersję twierdzenia 2.
Twierdzenie 4
Jeśli w haszowaniu liniowym h jest wybrana losowo z dowolnej 5-niezależnej rodziny funkcji haszujących H, oraz m≥3n, to oczekiwany czas operacji słownikowych jest stały.
Zauważmy, że przyjęliśmy α≤13. Spójną składową tablicy T będziemy nazywać dowolny maksymalny ciąg kolejnych niepustych komórek tablicy T. Niech cc(p) oznacza spójną składową zawierającą komórkę p, natomiast |cc(p)| jej długość. Łatwo widać, że prawdziwy jest następujący lemat.
Lemat 5
Czas działania każdej z operacji insert(x), lookup(x) oraz delete(x) można oszacować przez O(|cc(h(x))|). ♦
Weźmy dowolne x∈U. Zgodnie z lematem 5, aby wykazać twierdzenie 4 wystarczy, że oszacujemy przez pewną stałą oczekiwaną długość spójnej składowej zawierającej pozycję h(x). Oczywiście zawsze istnieje takie k, że |cc(h(x))|∈{2k,…,2k+1−1}. Podzielmy komórki T na równe bloki długości 2k−2.
Przypomnijmy, że notacja 1E, gdzie E jest dowolnym zdarzeniem oznacza indykator zdarzenia E, czyli zmienną losową która przyjmuje wartość 1 gdy E zaszło i 0 w przeciwnym przypadku (formalnie, 1E(ω)=[ω∈E]).
Lemat 6
E[|h(S)∩B|]=α|B|=13|B|
Dowód
Korzystamy z liniowości wartości oczekiwanej i jednostajności H, czyli faktu, że P[h(e)=b]=1/m dla dowolnych e∈U, b∈[m]. Zauważmy, że E[|h(S)∩B|]=∑e∈SE[1h(e)∈B]=∑e∈SP[h(e)∈B]=∑e∈S∑b∈BP[h(e)=b]=n⋅|B|⋅1m=α|B|=13|B|. ♦
Powiemy, że blok B={i,i+1,…,i+2k−2−1} jest niebezpieczny, gdy |h(S)∩B|≥23|B|. Uwaga! Zauważmy, że nie liczymy tu ile komórek B jest zajętych, a jedynie do ilu komórek B haszują się elementy S.
Lemat 7
Jeśli h(x) jest w spójnej składowej długości ∈{2k,…,2k+1−1} to jeden z O(1) bloków przecinających cc(h(x)) jest niebezpieczny.
Dowód
Oznaczmy kolejne bloki przecinające cc(h(x)) przez B1,…,Bk. Zauważmy, że 4≤k≤9. Załóżmy przeciwnie, że wszystkie te bloki są bezpieczne. W szczególności oznacza to, że mniej niż 232k−2 elementów haszujących do B1 znajduje się w kolejnych blokach. W blokach B2 i B3 jest sumarycznie więcej niż 2⋅13|B| komórek, które nie zawierają elementów haszujących do B2 i B3. To oznacza, że nie wszystkie z tych komórek zostaną zajęte przez elementy haszujące do B1, a więc co najmniej jedna komórka pozostanie pusta (gdyż inne elementy nie mogą tam się pojawić). W takim razie k≤3, sprzeczność. ♦
Załóżmy teraz, że znamy wartość ρ=h(x) i chcemy oszacować prawdopodobieństwo, że |cc(ρ)|∈{2k,…,2k+1−1}.
Ponieważ cc(ρ) przecina co najwyżej 9 bloków, to jest co najwyżej 17 bloków B1,…,Bk, które potencjalnie mogą przecinać się z cc(ρ). Z lematu 7 wiemy, że jeśli |cc(ρ)|∈{2k,…,2k+1−1} to jeden z tych bloków jest niebezpieczny.
Stąd, P[|cc(ρ)|∈{2k,…,2k+1−1}|h(x)=ρ]≤k∑i=1P[|h(S)∩Bi|≥23|Bi||h(x)=ρ].
Z symetrii i lematu 6 mamy dalej (*)
P[|cc(ρ)|∈{2k,…,2k+1−1}|h(x)=ρ]=O(1)⋅P[|h(S)∩B|≥E(|h(S)∩B|)+13|B||h(x)=ρ],
gdzie B jest pewnym konkretnym blokiem długości 2k−2.
Od tej pory, dla uproszczenia zakładamy, że wszsytko jest warunkowane przez zdarzenie h(x)=ρ i będziemy pomijać w prawdopodobieństwach (i wartościach oczekiwanych) napis ,,|h(x)=ρ''. Zauważmy, że przy tym warunkowaniu, rodzina H jest 4-niezależna.
Niech Xe=1h(e)∈B oraz X=∑e∈SXe. Przy takiej notacji, chodzi nam o to, żeby oszacować P[X>2E(X)]. Narzuca się, żeby w celu oszacowania powyższego prawdopodobieństwa użyć nierówności Chernoffa, problem polega jednak na tym, że zmienne Xe nie są niezależne (a jedynie 4-niezależne). Z nierówności Markowa dostajemy szacowanie przez 12, lecz jak się później okaże jest ono o wiele za słabe. Kolejny pomysł to użycie nierówności Czebyszewa -- dałaby ona lepsze oszacowanie, lecz w dalszym ciągu niewystarczające. Okazuje się, że wystarczy zrobić jeden krok dalej -- mianowicie użyć następującego faktu:
Nierówność czwartego momentu: P[|X−EX|>d]≤E[(X−EX)4]d4.
Dowód nierówności
Dowodzimy tak samo jak nierówność Czebyszewa, tylko podnosimy do 4-tej potęgi:
P[|X−EX|>d]=P[(X−EX)4>d4]≤E[(X−EX)4]d4,
gdzie ostatnia nierówność wynika z nierówności Markowa.♦
Pozostaje jedynie oszacować czwarty moment, czyli E[(X−EX)4]. Oznaczmy
Dla dowolnego e i parzystego j>0 mamy
\begin{align}
\mathbb{E}[Y_e^j] &= \left(1-\tfrac{|B|}{m}\right)^j \tfrac{|B|}{m} + \left(-\tfrac{|B|}{m}\right)^j\left(1-\tfrac{|B|}{m}\right) =
\left(1-\tfrac{|B|}{m}\right)^j \tfrac{|B|}{m} + \left(\tfrac{|B|}{m}\right)^j\left(1-\tfrac{|B|}{m}\right) =\\
&= \tfrac{|B|}{m} \left(1-\tfrac{|B|}{m}\right) \left(\left(1-\tfrac{|B|}{m}\right)^{j-1} + \left(\tfrac{|B|}{m}\right)^{j-1}\right) < \tfrac{|B|}{m}.
\end{align}
Stąd, pamiętając o oznaczeniu \alpha = n/m,
\mathbb{E}[Y^4] < n \frac{|B|}{m} + 6\frac{n^2}{2}\left(\frac{|B|}{m}\right)= \alpha|B| + 3(\alpha|B|)^2 < 4(\alpha|B|)^2.
Stąd i z nierówności 4-tego momentu mamy, że (**)
\mathbb{P}[X>2\mathbb{E}X] = \mathbb{P}[X-\mathbb{E}X > \mathbb{E}X] < \frac{4(\alpha|B|)^2}{(\alpha|B|)^4} = \frac{4}{\alpha^4}|B|^{-2}=O(|B|^{-2}).
Stąd i z (*) mamy
\begin{align} \mathbb{E}[|cc(\rho)| | h(x)=\rho] &= \sum_l l\cdot \mathbb{P}[|cc(\rho)|=l | h(x)=\rho]\\ & < \sum_k 2^{k+1}\cdot \mathbb{P}[|cc(\rho)|\in\{2^k,\ldots,2^{k+1}-1\} | h(x)=\rho]\\ & <^{(*), (**)} \sum_k 2^{k+1}\cdot O(1)\cdot (2^{k-2})^{-2} \\ & = O(1) \sum_k 2^{-k} \\ & = O(1). \end{align}
Ponieważ jest to prawdziwe dla dowolnego \rho\in [m], więc \mathbb{E}[|cc(h(x))|]=O(1), a tym samym dowód twierdzenia 4 jest zakończony.
Literatura
Wynik przedstawiony w tym wykładzie został po raz pierwszy opisany w pracy
A. Pagh, R. Pagh, M. Ruzic. Linear probing with constant independence. SIAM J. Comput., 39(3):1107–1120, 2009.
Użyty tutaj sposób prezentacji dowodów jest jednak w głównej mierze inspirowany blogiem Mihai Pătraşcu oraz pracą
M. Pătraşcu, M. Thorup. The Power of Simple Tabulation Hashing, Proc. 43rd ACM Symposium on Theory of Computing (STOC 2011).
Twierdzenie 3 zostało udowodnione w pracy
M. Patrascu and M. Thorup. On the -independence required by linear probing and minwise independence. Proc. ICALP 2010.