Pobierz prezentację
Pobieranie prezentacji. Proszę czekać
1
Haszowanie Jakub Radoszewski
2
Postawienie problemu Poszukujemy struktury danych, z pomocą której moglibyśmy efektywnie wykonywać operacje: wstawianie obiektów, usuwanie obiektów, wyszukiwanie obiektów. Obiekty: liczby, napisy, rekordy z danymi personalnymi. Zastosowania: bazy danych, sieci komputerowe, ...
3
Obiekty to liczby z przedziału [0, N-1]
Szukana struktura to prosta tablica N-elementowa: bool t[N]; Wstawianie: void wstaw(int n) { t[n] = true; } Usuwanie: void usun(int n) { t[n] = false; Wyszukiwanie: bool wyszukaj(int n) { return t[n];
4
Przypadek ogólny Szukamy funkcji h, przekształcającej obiekty w liczby z przedziału [0,N-1]. h to funkcja haszująca (funkcja skrótu). Obiekty mogą być parami (klucz,wartość), np. (imię i nazwisko, informacje o przelewach bankowych). Funkcja haszująca może operować tylko na kluczach. Mamy teraz tablicę obiektów: obiekt t[N]; Przy wstawianiu wykorzystujemy funkcję h: void wstaw(obiekt o) { int hasz = h(o.klucz); t[hasz] = o; }
5
Przypadek ogólny cd. Usuwanie wygląda podobnie: (BRAK jest jakąś stałą, oznaczającą brak obiektu) void usun(obiekt o) { int hasz = h(o.klucz); t[hasz] = BRAK; } Wyszukiwać możemy np. po kluczu: obiekt wyszukaj(klucz k) { int hasz = h(k); return t[hasz]; Wniosek: o ile wyznaczanie h(klucz) jest szybkie, to wstawianie, usuwanie i wyszukiwanie też są szybkie!
6
Funkcje haszujące Pytanie: skąd wziąć odpowiednią funkcję haszującą?
Załóżmy, że klucze są (dużymi) liczbami, np. z zakresu (typ int w C++). Przykłady wykorzystywanych funkcji haszujących: h(x) = x % p, gdzie p jest pierwsze h(x) = (x*p + r) % q, gdzie p i q są pierwsze h(x) = [((x*A) mod 1)*m], gdzie A jest z przedziału (0,1)
7
Problem kolizji Liczb z zakresu -109..109 jest znacznie więcej, niż N.
Zasada szufladkowa Dirichleta podpowiada, że mogą być kolizje. Chcemy, żeby h była bardzo „losowa”, żeby dobrze rozrzucała klucze. Paradoks urodzin: Jeżeli w jednej sali jest co najmniej (= ok. 19) osób, to z prawdopodobieństwem ½ dwie z nich obchodzą urodziny tego samego dnia. Wniosek: nawet przy bardzo losowej funkcji haszującej, już po wstawieniach elementów jest duża szansa na wystąpienie kolizji! Istnieje kilka sposobów zaradzenia tej sytuacji.
8
Adresowanie otwarte Pomysł: jeżeli dane miejsce w tablicy jest zajęte, to spróbuj obiekt umieścić gdzieś indziej. Adresowanie liniowe: jeżeli pozycja h(k) jest zajęta, spróbuj umieścić obiekt na pozycji h(k)+1, w razie kolejnej porażki – na pozycji h(k)+2, ... Adresowanie kwadratowe: jeżeli pozycja h(k) jest zajęta, to próbuj (do skutku) umieszczać dany obiekt na pozycjach h(k)+12, h(k)-12, h(k)+22, h(k)-22, h(k)+32, ... Haszowanie dwukrotne (rehaszowanie): stanowi rozszerzenie dwóch pierwszych pomysłów. Rozważane są mianowicie pozycje h(k)+g(k), h(k)+2*g(k), ..., gdzie g jest jakąś inną funkcją haszującą.
9
Metoda łańcuchowa Dla każdej wartości hasza z przedziału [0,N-1] w tablicy t przechowujemy zbiór obiektów, których klucze mają taką samą wartość hasza. Pytanie: jak reprezentować zbiór obiektów, którego rozmiaru nie potrafimy przewidzieć? Rozwiązanie: dynamiczne struktury danych (np. listy). W C++ da się prościej za pomocą vectora. vector to jeden z kontenerów z biblioteki standardowej C++ (tzw. STL). Zapewne o STL-u nieraz jeszcze usłyszycie.
10
vector w C++ Potrzebujemy załadować plik nagłówkowy:
#include <vector> using namespace std; vector działa jak tablica zmiennego rozmiaru: vector<int> v; deklaracja vectora intów (ogólnie vector<typ>) UWAGA: vector domyślnie jest pusty! v.push_back(123); wstawienie elementu 123 na koniec vectora, co powoduje jego powiększenie v.pop_back(); usunięcie ostatniego elementu (zmniejszenie) v[7] = 5; działa jak dla tablicy, o ile ósmy element v istnieje int rozm = v.size(); zwraca aktualny rozmiar vectora if (v.empty()) ... ; czy vector jest pusty? i wiele innych operacji...
11
Wykorzystanie vectora
Deklaracja tablicy t: vector<obiekt> t[N]; Wstawianie obiektu: void wstaw(obiekt o) { int hasz = h(o.klucz); t[hasz].push_back(o); } Usuwanie obiektu: void usun(obiekt o) { for (int i = 0; i < t[hasz].size(); i++) if (t[hasz][i] == o) { /* Przerzuć na koniec i usuń */ swap(t[hasz][i], t[hasz][t[hasz].size()-1]); t[hasz].pop_back();
12
Wykorzystanie vectora cd.
Wyszukiwanie obiektu: obiekt wyszukaj(klucz k) { int hasz = h(k); for (int i = 0; i < t[hasz].size(); i++) if (t[hasz][i].klucz == k) return t[hasz][i]; return BRAK; } Podsumowanie wykorzystania vectora: korzyści: znika problem kolizji, straty: usuwanie i wyszukiwanie mogą być wolne.
13
Zastosowania haszowania
Problem przechowywania haseł w komputerach: niebezpiecznie to robić w czystym tekście! W komputerze przechowuje się tylko hasze z haseł. Funkcja haszująca jest dosyć skomplikowana (algorytm DES, będący właściwie metodą szyfrowania). Ważne, żeby funkcja haszująca była „losowa” i „nieprzewidywalna”, aby trudno było do danego hasza podać jakiekolwiek dobre hasło (ważne w przypadku przechwycenia maszyny). Tę własność określa się także mianem jednokierukowości.
14
Zastosowania haszowania
Problem zakłóceń w transmisji: przy wysyłaniu dużych plików przez Internet gubią się ich fragmenty (pakiety). Do sprawdzenia poprawności transmisji używa się dodatkowo przesłanego hasza (wyliczonego algorytmem MD5 bądź odmianą algorytmu CRC), będącego sumą kontrolną pliku. Odbiorca wyznacza hasz z otrzymanego pliku i sprawdza, czy wyszedł taki sam jak otrzymany. Hasz musi być małych rozmiarów, żeby zminimalizować transmisję nadmiarowych danych. Funkcja haszująca musi być „losowa”, żeby efekt utraty kilku pakietów z dużą pewnością powodował zmianę jej wartości.
15
Zastosowania haszowania
Problem wyszukiwania wzorca (np. słowa, wyrażenia) w tekście. Pojawia się w edytorach tekstu (np. Word), przeglądarkach internetowych (Internet Explorer, Mozilla Firefox, ...). Tym zajmiemy się dokładniej. Ale najpierw pytanie: Jak wyznaczać hasze z napisów? (np. łańcuchów w C++) Dla napisu postaci char a[N] stosuje się np. funkcję: h(a) = (a[0] + a[1]*p + a[2]*p a[N-1]*pN-1) % q, gdzie p i q są dosyć dużymi liczbami pierwszymi (wtedy funkcja działa najlepiej, czyli minimalizuje liczbę kolizji).
16
Wyszukiwanie wzorca Algorytm naiwny: sprawdza wszystkie pozycje tekstu, na których mógłby się zaczynać wzorzec: char w[N], t[M]; /* wzorzec i tekst */ for (int i = 0; i <= M – N; i++) { /* Szukamy wystąpienia wzorca na pozycjach i, i+1, ..., i+N-1. */ bool znalazlem = false; for (int j = 0; j < N; j++) if (w[j] != t[i + j]) { jest = false; break; /* Wczesne wyjście z pętli. */ } if (jest) printf(‘’Wystapienie na pozycji %d.\n”, i);
17
Analiza algorytmu naiwnego
Dla losowych danych (w oraz t) zachowuje się świetnie: jest bardzo szybki! Dla specyficznych danych może wykonywać ok. N*M operacji, np. dla danych typu: aaaaaaaaab tekst aaaab wzorzec, i=0 aaaab i=1 aaaab i=2 aaaab i=3 aaaab i=4 aaaab sukces, i=5
18
Hasze pomagają wyszukiwać
Krok 1: Liczymy hasz dla wzorca w ze wzoru: hasz = (w[0] + w[1]*p + w[2]*p w[N-1]*pN-1) % q za pomocą takiej oto prostej pętli: int hasz = 0; for (int i = N – 1; i >= 0; i--) hasz = (hasz * p + w[i]) % q; To działa, bo kolejno otrzymywane wartości hasza to: 0, (w[N-1])%q, (w[N-2] + w[N-1]*p)%q, (w[N-3] + w[N-2]*p + w[N-1]*p2)%q, ... Ciekawostka: ta metoda nazywa się schematem Hornera.
19
Hasze pomagają wyszukiwać
Krok 2: liczymy hasze dla tekstu, ale tym razem spamiętujemy wszystkie wartości pośrednie w tablicy: int hasze[M + 1]; hasze[M] = 0; for (int i = M – 1; i >= 0; i--) hasze[i] = (hasze[i + 1] * p + t[i]) % q; Zawartość tablicy hasze to teraz: (podobnie jak dla wzorca) hasze[0] = (t[0] + t[1]*p + t[2]*p t[M-1]*pM-1) % q, hasze[1] = (t[1] + t[2]*p t[M-1]*pM-2) % q, ... hasze[M-2] = (t[M-2] + t[M-1]*p) % q, hasze[M-1] = (t[M-1]) % q, hasze[M] = 0.
20
Hasze pomagają wyszukiwać
Jak teraz sprawdzić, czy wzorzec w występuje w tekście t na pozycjach i, i+1, ..., i+N-1? Musimy policzyć jakoś hasz dla słowa t[i..i+N-1] i porównać go z haszem dla w. Szukanym haszem okazuje się być wartość: (hasze[i] – hasze[i+N]*pN) % q = ((t[i] + t[i+1]*p t[i+N]*pN + t[i+N+1]*pN+1 + t[M-1]*pM-1-i) – (t[i+N] + t[i+N+1]*p t[M-1]*pM-1-i-N)*pN) % q = (t[i] + t[i+1]*p t[i+N-1]*pi+N-1) % q.
21
Ostateczna postać wyszukiwania
Oto ostateczny pseudokod algorytmu wyszukiwania wzorca w tekście z wykorzystaniem haszy (zakładamy, że policzyliśmy już wartość hasz dla wzorca oraz tablicę hasze dla tekstu): pN = 1; /* Chcemy, by to było równe pN */ for (int i = 1; i <= N; i++) pN = (pN * p) % q; for (int i = 0; i <= M – N; i++) { /* Szukamy wystąpienia wzorca na pozycjach i, i+1, ..., i+N-1. */ if (hasz == (hasze[i] – hasze[i + N] * pN) % q) printf(‘’Wystapienie na pozycji %d.\n”, i); } Wyszukiwanie zajmuje około M-N operacji, a samo liczenie haszy: łącznie około N+M operacji. To dużo lepiej niż wynik algorytmu naiwnego! Za to ryzykujemy możliwość kolizji...
22
Uwagi końcowe Przykład dobrych liczb pierwszych:
p = , q = W takim przypadku mogą wystąpić problemy przy mnożeniu (np. hasze[i + N] * pN), gdyż typ int się przepełnia (wynikiem może być liczba rzędu nawet 1018! ) Rozwiązaniem jest zastosowanie przy mnożeniu większego typu, np. long long: (long long)(hasze[i + N]) * pN który ma dokładność jeszcze troszkę większą niż 1018.
Podobne prezentacje
© 2024 SlidePlayer.pl Inc.
All rights reserved.