Algorytmy i struktury danych/Wyszukiwanie: Różnice pomiędzy wersjami
| Linia 158: | Linia 158: | ||
jednej operacji może wynosić nawet <math>O(n)</math>, jednak w praktycznych | jednej operacji może wynosić nawet <math>O(n)</math>, jednak w praktycznych | ||
zastosowaniach ta metoda sprawuje się doskonale. | zastosowaniach ta metoda sprawuje się doskonale. | ||
W tym rozdziale będziemy zakładać, że elementy uniwersum <math>U</math>, | |||
to dodatnie liczby całkowite. | |||
Dodatkowo zakładamy, że dysponujemy tablicą <math>A[0,\ldots,m-1]</math>. | |||
Ponieważ elementami mogą być bardzo duże liczby całkowite, stąd nie możemy | |||
zastosować metody adresowania bezpośredniego. | |||
Jednak możemy wybrać '''funkcję haszującą''': | |||
<center> | |||
<math>h : U \rightarrow 0,\ldots,m-1</math> | |||
</center> | |||
Funkcja ta dla każdego elementu uniwersum przypisuje | |||
odpowiednie miejsce w tablicy <math>A</math>. Jeśli <math>|U|>m</math>, to | |||
z oczywistych względów znajdą się takie pary różnych elementów <math>x,y\in U</math>, | |||
dla których <math>f(x)=f(y)</math>. W takim przypadku mówimy o istnieniu '''kolizji'''. | |||
Właśnie ze względu na ryzyko wystąpienia kolizji, musimy nieznacznie zmodyfikować metodę | |||
adresowania bezpośredniego - zamiast przechowywać w tablicy wartość logiczną (prawda/fałsz), | |||
musimy zapisywać wartość przechowywanego elementu. | |||
=== Rozwiązywanie kolizji metodą łańcuchową === | |||
Jedną z metod rozwiązywania kolizji jest utrzymywanie w każdej komórce tablicy | |||
listy elementów do niej przypisanych. | |||
Początkowo tablica <math>A</math> wypełniona jest wartościami '''nil'''. | |||
'''procedure''' Inicjalizacja; | |||
'''begin''' | |||
'''for''' i:=0 '''to''' m-1 '''do''' A[i]:='''nil'''; | |||
'''end'''; | |||
Dodanie elementu <math>x</math> do tablicy, wymaga odczytania jego adresu z funkcji haszującej, | |||
a następnie dodania go na początek listy <math>A[h(x)]</math> | |||
'''procedure''' Dodaj(x); | |||
'''begin''' | |||
dodaj element <math>x</math> na początek listy <math>A[h(x)]</math> | |||
'''end'''; | |||
Sprawdzenie czy element <math>x</math> istnieje, wymaga w pesymistycznym przypadku sprawdzenia całej listy <math>A[h(x)]</math> | |||
'''function''' Szukaj(x); | |||
'''begin''' | |||
l:=A[h(x)]; | |||
'''while''' (l!='''nil''') '''do''' '''begin''' | |||
'''if''' (l.wartosc=x) '''then''' '''return''' element istnieje | |||
l:=l.nast; | |||
'''end'''; | |||
'''return''' brak elementu | |||
'''end'''; | |||
Usuwanie elementu z tablicy jest bardzo podobne do wyszkiwania, i również w pesymistycznym przypadku wymaga sprawdzenia całej listy. | |||
'''procedure''' Usun(x); | |||
'''begin''' | |||
l:=A[h(x)];p:=nil; | |||
'''while''' (l!='''nil''') '''do''' '''begin''' | |||
'''if''' (l.wartosc=x) '''then''' '''begin''' | |||
{ usuwamy element l z listy A[h(x)] } | |||
'''if''' p='''nil''' '''then''' A[h(x)]:=l.nast; | |||
'''else''' p.nast:=l.nast; | |||
'''return'''; | |||
'''end''' | |||
p:=l; l:=l.nast; | |||
'''end'''; | |||
'''end'''; | |||
=== Wybór funkcji haszujących === | |||
=== Adresowanie otwarte === | |||
TODO | TODO | ||
* metody rozwiązywania konfliktów | * metody rozwiązywania konfliktów | ||
* haszowanie podwójne | * haszowanie podwójne | ||
Wersja z 08:40, 8 sie 2006
Wyszukiwanie
W niniejszym wykładzie opiszemy podstawowe techniki dotyczące wyszukiwania.
Zajmiemy się również prostymi strukturami słownikowymi, które oprócz
wyszukiwania, umożliwiają dodawanie i usuwanie elementów.
Wyszukiwanie liniowe
Jeśli nie dysponujemy żadną dodatkową więdzą na temat zbioru, który chcemy przeszukiwać, to niestety musimy sprawdzić wszystkie jego elementy.
function Szukaj(x, A[1..n])
begin
for i:=1 to n do
if A[i]=x return i;
return brak poszukiwanego elementu;
end
Oczywiście w pesymistycznym przypadku (np. gdy zbiór nie zawiera poszukiwanego elementu) koszt czasowy, to .
Wyszukiwanie binarne
W niektórych przypadkach czas poszukiwania, możemy znacząco zmniejszyć. Dzieje się tak na przykład, gdy przeszukiwany zbiór przechowujemy w rosnąco uporządkowanej tablicy. W takim przypadku, wystarczy jedynie operacji, by odnaleźć poszukiwany element, lub stwierdzić jego brak.
Algorytm utrzymuje zakres , w którym może znajdować się element, przy każdym porównaniu zakres zostaje zmniejszony o połowę.
function WyszukiwanieBinarne(x, A[1..n])
{ zakładamy, że tablica A, jest uporządkowana rosnąco }
begin
l:=1;r:=n;
while (l<=r) do begin
{ niezmiennik, poszukiwany element, może znajdować się w zakresie A[l..r] }
m:=(l+r) div 2;
if (A[m]<x) then l:=m+1
else if (A[m]>x) then r:=m-1
else return m; { ponieważ A[m]=x }
end;
return brak poszukiwanego elementu;
end;
Drzewa poszukiwań binarnych
Drzewa poszukiwań binarnych, to zwykłe drzewa binarne, których klucze spełniają następujące własności:
Dla dowolnego węzła x:
- wszystkie klucze w lewym poddrzewie węzła x, mają wartości mniejsze niż klucz węzła x,
- wszystkie klucze w lewym poddrzewie węzła x, mają wartości większe lub równe niż klucz węzła x.
TODO -- przykładowe drzewo
Dodatkowe wymaganie dotyczące kluczy, umożliwia nam efektywne wszyskiwanie elementów w drzewie.
function Szukaj(wezel, klucz)
if (wezel==nil)
return BRAK ELEMENTU
if (wezel.klucz=klucz) then
return ELEMENT ISTNIEJE
else if (klucz < wezel.klucz) then
return Szukaj(wezel.lewePoddrzewo, klucz)
else if (klucz > wezel.klucz) then
return Szukaj(wezel.prawPoddrzewo, klucz)
end;
Wstawianie do drzewa jest bardzo zbliżone do wyszukiwania, musimy przejść po drzewie (rozpoczynająć w korzeniu) aby odnaleźć wolne miejsce w którym możemy dodać nową wartość.
procedure Dodaj(wezel, klucz)
if (klucz < wezel.klucz) then
if wezel.lewePoddrzewo=nil then
utwórz nowy węzeł z wartością klucz
wskaźnik do nowego węzła zapisujemy w wezel.lewePoddrzewo
else
Dodaj(wezel.lewePoddrzewo, klucz)
else if (klucz >= wezel.klucz) then
if wezel.prawePoddrzewo=nil then
utwórz nowy węzeł z wartością klucz
wskaźnik do nowego węzła zapisujemy w wezel.prawePoddrzewo
else
Dodaj(wezel.prawePoddrzewo, klucz)
end;
Możemu również usuwać wartości z drzewa, niestety ta operacja jest bardziej skomplikowana.
procedure Usun(wezel, klucz)
if (klucz < wezel.klucz) then
Usun(wezel.lewePoddrzewo, klucz)
else if (klucz > wezel.klucz) then
Usun(wezel.prawePoddrzewo, klucz)
else begin { klucz = wezel.klucz
if wezel jest liściem, then
{ usuń wezel z drzewa }
UsunProstyPrzypadek(wezel)
else
if wezel.lewePoddrzewo <> nil then
niech x oznacza skrajnie prawy węzeł w poddrzewie wezel.lewePoddrzewo
wezel.klucz:=x.klucz;
UsunProstyPrzypadek(x);
'else
analogiczne postępowanie dla wezel.prawPoddrzewo
(jednak poszukujemy węzła na skrajnie lewej ścieżce)
end
Procedura UsunProstyPrzypadek oznacza usuwanie z drzewa węzła, który ma co najwyżej jednego syna.
procedure UsunProstyPrzypadek(wezel)
poddrzewo:=nil;
ojciec:=wezel.ojciec;
if (wezel.lewePoddrzewo) then
poddrzewo:=wezel.lewePoddrzewo;
else
poddrzewo:=wezel.prawePoddrzewo;
if (ojeciec=nil) then
korzen:=poddrzewo;
else if ojciec.lewePoddrzewo=wezel then { węzeł jest lewym synem }
ojciec.lewePoddrzewo:=podrzewo;
else { węzeł jest prawym synem }
ojciec.prawePoddrzewo:=podrzewo;
Wszystkie podane operacje mają pesymistyczny koszt gdzie oznacza wysokość drzewa. Niestety w nagorszym przypadku drzewo może mieć wysokość nawet (np. dla ciągu operacji Dodaj ).
Adresowanie bezpośrednie
W przypadku gdy zbiór który przechowujemy pochodzi z niewielkiego uniwersum (na przykład elementy zbioru to liczby z zakresu ), możemy wszyskie operacje słownikowe (dodaj, usuń, szukaj) wykonać znacznie szybciej i prościej.
Dla uniwersum zbiór możemy reprezentować przez tablicę -elementową. Początkowo w każdej komórce tablicy wpisujemy wartość false.
- dodanie elementu do zbioru, wymaga jedynie ustawienia wartości -tej komórki na true,
- analogicznie usunięcie elementu do zbioru, wymaga ustawienia wartości -tej komórki na false,
- sprawdzenie czy element należy do zbioru wykonujemy przez sprawdzenie stanu -tej komórki.
Wszystkie powyższe operacje możemy wykonać używając stałej liczby kroków.
Haszowanie
Czy możemy wykorzystać adresowanie bezpośrednie do dowolnych zbiorów? Okazuje się, że tak. Co prawda w pesymistycznym przypadku koszt jednej operacji może wynosić nawet , jednak w praktycznych zastosowaniach ta metoda sprawuje się doskonale.
W tym rozdziale będziemy zakładać, że elementy uniwersum , to dodatnie liczby całkowite. Dodatkowo zakładamy, że dysponujemy tablicą .
Ponieważ elementami mogą być bardzo duże liczby całkowite, stąd nie możemy zastosować metody adresowania bezpośredniego. Jednak możemy wybrać funkcję haszującą:
Funkcja ta dla każdego elementu uniwersum przypisuje odpowiednie miejsce w tablicy . Jeśli , to z oczywistych względów znajdą się takie pary różnych elementów , dla których . W takim przypadku mówimy o istnieniu kolizji. Właśnie ze względu na ryzyko wystąpienia kolizji, musimy nieznacznie zmodyfikować metodę adresowania bezpośredniego - zamiast przechowywać w tablicy wartość logiczną (prawda/fałsz), musimy zapisywać wartość przechowywanego elementu.
Rozwiązywanie kolizji metodą łańcuchową
Jedną z metod rozwiązywania kolizji jest utrzymywanie w każdej komórce tablicy listy elementów do niej przypisanych. Początkowo tablica wypełniona jest wartościami nil.
procedure Inicjalizacja; begin for i:=0 to m-1 do A[i]:=nil; end;
Dodanie elementu do tablicy, wymaga odczytania jego adresu z funkcji haszującej, a następnie dodania go na początek listy
procedure Dodaj(x); begin dodaj element na początek listy end;
Sprawdzenie czy element istnieje, wymaga w pesymistycznym przypadku sprawdzenia całej listy
function Szukaj(x);
begin
l:=A[h(x)];
while (l!=nil) do begin
if (l.wartosc=x) then return element istnieje
l:=l.nast;
end;
return brak elementu
end;
Usuwanie elementu z tablicy jest bardzo podobne do wyszkiwania, i również w pesymistycznym przypadku wymaga sprawdzenia całej listy.
procedure Usun(x);
begin
l:=A[h(x)];p:=nil;
while (l!=nil) do begin
if (l.wartosc=x) then begin
{ usuwamy element l z listy A[h(x)] }
if p=nil then A[h(x)]:=l.nast;
else p.nast:=l.nast;
return;
end
p:=l; l:=l.nast;
end;
end;
Wybór funkcji haszujących
Adresowanie otwarte
TODO
- metody rozwiązywania konfliktów
- haszowanie podwójne
- haszowanie doskonałe