Algorytmy i struktury danych Tablice haszowane
Klucz Klucz – „wyróżnik danych” np. pesel, nr dowodu, nr indeksu data ur.+imię +nazwisko,, Założymy, że klucze to liczby, zawsze można zbudować funkcję transformującą np. napis na liczbę: B. proste (i niespecjalnie dobre) rozwiązanie: można np. zsumować kody odpowiadające literom „ALA” -> 65+76+65
Tablica z adresowaniem bezpośrednim ! Małe uniwersum kluczy ! 1 2 Klucz Dane 3 Klucz Dane 4 2 3 5 Klucz Dane K 6 5 7 8 8 Klucz Dane 9
Adresowanie bezpośrednie - implementacja def DirectAdress_Search(table, key): return table[key] def DirectAdress_Insert(table, data): table[data.key] = data def DirectAdress_Delete(table, key): table[key] = None w Pythonie [] nie jest tablicą
Tablica z funkcją haszującą Funkcja haszująca odwzorowuje klucz w przestrzeń adresową tablicy 1 2 Klucz Dane 3 Klucz Dane Uniwersum wszystkich kluczy 4 k2 k3 5 Klucz Dane K 6 k5 7 k8 8 Klucz Dane 9
Funkcja haszująca F. haszująca - odwzorowuje klucz w przestrzeń adresową (zwykle mniejszą niż uniwersum), tj. dziedzinę poprawnych adresów tablicy h(k) powinno należeć do przestrzeni adresowej tablicy dla dowolnego (legalnego) klucza Wymagania: generowanie adresów dla rzeczywistego zbioru rekordów w sposób jak najbardziej równomierny (rozproszony) łatwość obliczenia Wskazane jest sprawdzenie wybranej f. haszująca na fragmencie rzeczywistych danych
Haszowanie modularne h(k) = k mod m, gdzie m jest rozmiarem tablicy Dobre m nie powinno być równe 2p, 10p – gdyż takie haszowanie ignoruje bardzo znaczące bity (cyfry) Dobre m nie powinno być równe 2p-1, 10p-1 gdyż daje identyczne wartości na ciągach, dla których przestawiono bity (cyfry) Niezłe są np. liczby pierwsze niezbyt bliskie potęgom 2
Haszowanie przez mnożenie h(k) = m (kA mod 1), gdzie kA mod 1 = kA - kA A zwykle jest liczbą z przedziału 0..1 m jest zwykle potęgą 2 lub 10, gdyż wtedy h(k) = pewna ilość cyfr po przecinku z kA W praktyce bardzo dobre A to np. (sqrt(5)-1)/2 = 0.6180339887....
Haszowanie przez randomizację h(k) = Rand (k) Metoda kwadratu środka: wydziel z klucza pewną jego część (np. środek), potraktuj jako liczbę binarną, po czym podnieś ją do kwadratu. Metoda składania: podziel klucz na części (segmenty), potraktuj je jako liczby binarne, po czym dodaj je do siebie arytmetycznie. Metoda sumy modulo 2: podziel klucz na części (segmenty), potraktuj je jako ciągi bitów i dodaj je do siebie modulo 2. W praktyce dla uzyskania dobrej losowości wymagane sa duże wartości stad często stosuje się ta metode w połączeniu z h. modularnym
Haszowanie uniwersalne H – rodzina funkcji haszujących Na początku pracy losowo wybieramy jedną funkcję haszującą z całej rodziny i stosujemy ją od tej pory hH Brak możliwości „złośliwego” doboru elementów
Rozwiązywanie kolizji przez łańcuchowanie oddzielne Listy zawierają elementy, dla których h(k) jest takie samo-synon. Kolejność elementów na liście jest przypadkowa 1 2 k2 Dane k12 Dane 3 k3 Dane 4 k12 k2 k3 5 k15 Dane k5 Dane 6 k15 k5 7 k8 8 k8 Dane h(k) = k mod 10 9
Łańcuchowanie oddzielne – impl. def ChainHash_Search(table, key): return FindNode(table[h(key)], key) def ChainHash_Insert(table, data): pos = h(data.key) table[pos] = AddFirst(table[pos], data) def ChainHash_Delete(table, key): pos = h(x.key) table[pos] = RemoveNode(table[pos], key)
Łańcuchowanie oddzielne - właściwości - współczynnik zapełnienia = ilość elementów / rozmiar tablicy Pesymistyczny czas wyszukiwania = (n) Jeżeli h rozmieszcza klucze równomiernie, to dla Oczekiwany czas wyszukiwania = (1+ )
Adresowanie otwarte Dla rozwiązywania konfliktów nie stosuje się list Wszystkie elementy (wskaźniki) zapisywane są bezpośrednio w tablicy h(k, i) = ( h’(k) + g(i) ) gdzie: k – klucz, i – numer próby (0..N-1), N – rozmiar tablicy Aby umieścić element w tablicy sprawdzamy h(k, 0) jeśli zajęte, to h(k, 1) itd aż do h(k, N-1).
Adresowanie otwarte przykład kolizji 1 Kolejność dodawania: k12, k2, k4, k3 2 k12 Dane 3 k2 Dane 4 k4 Dane k12 k2 k3 5 k3 Dane 6 k4 7 8 h(k) = (k+i) mod 10 9
Adresowanie otwarte – implem. def Hash_Insert(table, data): for i in range(0, MAX): j = h(x.key, i) if table[j] == None: table[j] = data return ERROR „przepełnienie tablicy”
Adresowanie otwarte – implem. def Hash_Search(table, key): for in range(0, MAX): j = h(key, i) if table[j] == None: return None if table[j].key == key: return table[j]
Adresowanie otwarte – usuwanie elementów Problem: Nie można wpisywać None Rozwiązanie: Nowa stała np.: DELETED Należy wtedy zmodyfikować przeszukiwanie ......... if table[j] == None: return None if table[j] == DELETED: continue if table[j].key == key: return table[j] i dodawanie if table[j] == None or table[j] == DELETED:
Rodzaje adresowania otwartego Liniowe: h(k,i) = (h’(k) + i ) mod m Wadą jest grupowanie się elementów Kwadratowe: h(k,i) = (h’(k) + c1i + c2i2) mod m Wadą jest (mniej groźne) grupowanie wtórne Sześcienne: h(k,i) = (h’(k) + c1i + c2i2 + c3i3) mod m Dwukrotne: h(k,i) = (h1(k) + h2(i)) mod m Grupowanie wtórne jest znikome
Metoda otwarta - właściwości - współczynnik zapełnienia = ilość elementów / rozmiar tablicy Pesymistyczny czas wyszukiwania = (n) Dla liniowej funkcji g oczekiwany czas wyszukiwania z sukcesem 1+1/(1- ) z porażką to 1+1/(1- )2 Dla pseudolosowej funkcji g oczekiwany czas wyszuk. z sukcesem -1 (1+ln (1/(1- ))) z porażką to 1/(1- )
Uniwersum kluczy Powinno być nie mniejsze niż rozmiar tablicy Sumowanie kodów liter np. „ALA” -> 65+76+65 Stosunkowo mały rozmiar uniwersum, np. dla 30 znaków 30*256 = 7680 Sumowanie par (lub dłuższych ciągów liter), co daje większą przestrzeń adresów „ALA” -> 65*256+76 + 65*256 W uniwersum są „dziury” bo np. znaki mają kody >=32 i zwykle mniejsze niż 127 Suma iloczynów kodów par „ALA” -> 65*76 + 65 Nierównomierny rozkład wartości, np. więcej l. parzystych
Przykład Tablica 106 elementów adresowana nazwiskiem i imieniem Znaki: W ogólności jeden znak dla ASCII 0-255. 0-32 - znaki niedrukowalne, 32-47, 91-96, 123-127 znaczki różne, 48-57 cyfry, 65-90, 97-122 – litery, 128-255 znaki graficzne (polskie litery) UNICODE – jeden znak 0 – (216-1), dużo bardzo rzadko używanych znaków. Przy obliczeniach można pominąć znaki różne od liter (ewentualnie cyfr) Polskie znaki diakrytyczne: można zamienić na ich łacińskie odpowiedniki lub pominąć. Zamieniamy znaki na małe. Stąd jeden znak może mieć kod od 97 do 122. log122-97+1106 = log 26106 = 4.24. Czyli maksymalna waga dla znaku powinna wynosić co najmniej 265.
Przykład DIGITGROPUP = 5 BASE = ord("Z")-ord("A")+1 def Key(key): digitPos = 0 keyVal = 0 for c in key: c = ConvertChar(c) # konwersja na duze litery + # ew zamiana polskich znakow if c>="A" and c<="Z": digit = ord(c)-ord("A") keyVal = keyVal + digit * pow(BASE, digitPos) digitPos = (digitPos+1) % (DIGITGROPUP+1) return keyVal