Algorytmy i struktury danych/Wyszukiwanie: Różnice pomiędzy wersjami
Nie podano opisu zmian |
|||
Linia 11: | Linia 11: | ||
== Wyszukiwanie liniowe == | == Wyszukiwanie liniowe == | ||
Jeśli nie dysponujemy żadną dodatkową | Jeśli nie dysponujemy żadną dodatkową wiedzą na temat zbioru, który chcemy | ||
przeszukiwać, to niestety musimy sprawdzić wszystkie jego elementy. | przeszukiwać, to niestety musimy sprawdzić wszystkie jego elementy. | ||
Linia 61: | Linia 61: | ||
TODO -- przykładowe drzewo | TODO -- przykładowe drzewo | ||
Dodatkowe wymaganie dotyczące kluczy, umożliwia nam efektywne | Dodatkowe wymaganie dotyczące kluczy, umożliwia nam efektywne wyszukiwanie elementów w drzewie. | ||
'''function''' Szukaj( | '''function''' Szukaj(węzeł, klucz) | ||
'''if''' ( | '''if''' (węzeł=='''nil''') | ||
'''return''' BRAK ELEMENTU | '''return''' BRAK ELEMENTU | ||
'''if''' ( | '''if''' (węzeł.klucz=klucz) '''then''' | ||
'''return''' ELEMENT ISTNIEJE | '''return''' ELEMENT ISTNIEJE | ||
''' else if (klucz < | ''' else if (klucz < węzeł.klucz) '''then''' | ||
'''return''' Szukaj( | '''return''' Szukaj(węzeł.lewePoddrzewo, klucz) | ||
''' else if (klucz > | ''' else if (klucz > węzeł.klucz) '''then''' | ||
'''return''' Szukaj( | '''return''' Szukaj(węzeł.prawPoddrzewo, klucz) | ||
'''end'''; | '''end'''; | ||
Linia 77: | Linia 77: | ||
(rozpoczynająć w korzeniu) aby odnaleźć wolne miejsce w którym możemy dodać nową wartość. | (rozpoczynająć w korzeniu) aby odnaleźć wolne miejsce w którym możemy dodać nową wartość. | ||
'''procedure''' Dodaj( | '''procedure''' Dodaj(węzeł, klucz) | ||
'''if''' (klucz < | '''if''' (klucz < węzeł.klucz) '''then''' | ||
'''if''' | '''if''' węzeł.lewePoddrzewo=nil '''then''' | ||
utwórz nowy węzeł z wartością ''klucz'' | utwórz nowy węzeł z wartością ''klucz'' | ||
wskaźnik do nowego węzła zapisujemy w | wskaźnik do nowego węzła zapisujemy w węzeł.lewePoddrzewo | ||
'''else''' | '''else''' | ||
Dodaj( | Dodaj(węzeł.lewePoddrzewo, klucz) | ||
'''else if (klucz >= | '''else if (klucz >= węzeł.klucz) '''then''' | ||
'''if''' | '''if''' węzeł.prawePoddrzewo=nil '''then''' | ||
utwórz nowy węzeł z wartością ''klucz'' | utwórz nowy węzeł z wartością ''klucz'' | ||
wskaźnik do nowego węzła zapisujemy w | wskaźnik do nowego węzła zapisujemy w węzeł.prawePoddrzewo | ||
'''else''' | '''else''' | ||
Dodaj( | Dodaj(węzeł.prawePoddrzewo, klucz) | ||
'''end'''; | '''end'''; | ||
Możemy również usuwać wartości z drzewa, niestety ta operacja jest bardziej skomplikowana. | |||
'''procedure''' | '''procedure''' Usuń(węzeł, klucz) | ||
'''if''' (klucz < | '''if''' (klucz < węzeł.klucz) '''then''' | ||
Usuń(węzeł.lewePoddrzewo, klucz) | |||
'''else if (klucz > | '''else if (klucz > węzeł.klucz) '''then''' | ||
Usuń(węzeł.prawePoddrzewo, klucz) | |||
'''else''' '''begin''' { klucz = | '''else''' '''begin''' { klucz = węzeł.klucz | ||
'''if''' '' | '''if''' ''węzeł'' jest liściem, '''then''' | ||
{ usuń ''' | { usuń '''węzeł''' z drzewa } | ||
UsunProstyPrzypadek( | UsunProstyPrzypadek(węzeł) | ||
'''else''' | '''else''' | ||
'''if''' | '''if''' węzeł.lewePoddrzewo <> nil '''then''' | ||
niech ''x'' oznacza skrajnie prawy węzeł w poddrzewie | niech ''x'' oznacza skrajnie prawy węzeł w poddrzewie węzeł.lewePoddrzewo | ||
wezel.klucz:=x.klucz; | wezel.klucz:=x.klucz; | ||
UsunProstyPrzypadek(x); | UsunProstyPrzypadek(x); | ||
'''else'' | '''else'' | ||
analogiczne postępowanie dla | analogiczne postępowanie dla węzeł.prawPoddrzewo | ||
(jednak poszukujemy węzła na skrajnie lewej ścieżce) | (jednak poszukujemy węzła na skrajnie lewej ścieżce) | ||
'''end''' | '''end''' | ||
Linia 116: | Linia 116: | ||
ma co najwyżej jednego syna. | ma co najwyżej jednego syna. | ||
'''procedure''' UsunProstyPrzypadek( | '''procedure''' UsunProstyPrzypadek(węzeł) | ||
poddrzewo:=nil; | poddrzewo:=nil; | ||
ojciec:= | ojciec:=węzeł.ojciec; | ||
'''if''' ( | '''if''' (węzeł.lewePoddrzewo) '''then''' | ||
poddrzewo:= | poddrzewo:=węzeł.lewePoddrzewo; | ||
'''else''' | '''else''' | ||
poddrzewo:= | poddrzewo:=węzeł.prawePoddrzewo; | ||
'''if''' ( | '''if''' (ojciec=nil) '''then''' | ||
korzen:=poddrzewo; | korzen:=poddrzewo; | ||
'''else''' '''if''' ojciec.lewePoddrzewo= | '''else''' '''if''' ojciec.lewePoddrzewo=węzeł '''then''' { węzeł jest lewym synem } | ||
ojciec.lewePoddrzewo:= | ojciec.lewePoddrzewo:=poddrzewo; | ||
'''else''' { węzeł jest prawym synem } | '''else''' { węzeł jest prawym synem } | ||
ojciec.prawePoddrzewo:= | ojciec.prawePoddrzewo:=poddrzewo; | ||
Wszystkie podane operacje mają pesymistyczny koszt <math>O(h)</math> gdzie <math>h</math> | Wszystkie podane operacje mają pesymistyczny koszt <math>O(h)</math> gdzie <math>h</math> | ||
oznacza wysokość drzewa. Niestety w | oznacza wysokość drzewa. Niestety w najgorszym przypadku drzewo może mieć wysokość | ||
nawet <math>O(n)</math> (np. dla ciągu operacji Dodaj <math>(1,2,3,\ldots)</math>). | nawet <math>O(n)</math> (np. dla ciągu operacji Dodaj <math>(1,2,3,\ldots)</math>). | ||
Linia 139: | Linia 139: | ||
W przypadku gdy zbiór który przechowujemy pochodzi z niewielkiego uniwersum | W przypadku gdy zbiór który przechowujemy pochodzi z niewielkiego uniwersum | ||
(na przykład elementy zbioru to liczby z zakresu <math>1,\ldots,n</math>), | (na przykład elementy zbioru to liczby z zakresu <math>1,\ldots,n</math>), | ||
możemy | możemy wszystkie operacje słownikowe (dodaj, usuń, szukaj) wykonać | ||
znacznie szybciej i prościej. | znacznie szybciej i prościej. | ||
Linia 204: | Linia 204: | ||
l:=A[h(x)]; | l:=A[h(x)]; | ||
'''while''' (l!='''nil''') '''do''' '''begin''' | '''while''' (l!='''nil''') '''do''' '''begin''' | ||
'''if''' (l. | '''if''' (l.wartość=x) '''then''' '''return''' element istnieje | ||
l:=l.nast; | l:=l.nast; | ||
'''end'''; | '''end'''; | ||
Linia 210: | Linia 210: | ||
'''end'''; | '''end'''; | ||
Usuwanie elementu z tablicy jest bardzo podobne do | Usuwanie elementu z tablicy jest bardzo podobne do wyszukiwania, i również w pesymistycznym przypadku wymaga sprawdzenia całej listy. | ||
'''procedure''' | '''procedure''' Usuń(x); | ||
'''begin''' | '''begin''' | ||
l:=A[h(x)];p:=nil; | l:=A[h(x)];p:=nil; | ||
'''while''' (l!='''nil''') '''do''' '''begin''' | '''while''' (l!='''nil''') '''do''' '''begin''' | ||
'''if''' (l. | '''if''' (l.wartość=x) '''then''' '''begin''' | ||
{ usuwamy element l z listy A[h(x)] } | { usuwamy element l z listy A[h(x)] } | ||
'''if''' p='''nil''' '''then''' A[h(x)]:=l.nast; | '''if''' p='''nil''' '''then''' A[h(x)]:=l.nast; |
Wersja z 08:49, 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ą wiedzą 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 wyszukiwanie elementów w drzewie.
function Szukaj(węzeł, klucz) if (węzeł==nil) return BRAK ELEMENTU if (węzeł.klucz=klucz) then return ELEMENT ISTNIEJE else if (klucz < węzeł.klucz) then return Szukaj(węzeł.lewePoddrzewo, klucz) else if (klucz > węzeł.klucz) then return Szukaj(węzeł.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(węzeł, klucz) if (klucz < węzeł.klucz) then if węzeł.lewePoddrzewo=nil then utwórz nowy węzeł z wartością klucz wskaźnik do nowego węzła zapisujemy w węzeł.lewePoddrzewo else Dodaj(węzeł.lewePoddrzewo, klucz) else if (klucz >= węzeł.klucz) then if węzeł.prawePoddrzewo=nil then utwórz nowy węzeł z wartością klucz wskaźnik do nowego węzła zapisujemy w węzeł.prawePoddrzewo else Dodaj(węzeł.prawePoddrzewo, klucz) end;
Możemy również usuwać wartości z drzewa, niestety ta operacja jest bardziej skomplikowana.
procedure Usuń(węzeł, klucz) if (klucz < węzeł.klucz) then Usuń(węzeł.lewePoddrzewo, klucz) else if (klucz > węzeł.klucz) then Usuń(węzeł.prawePoddrzewo, klucz) else begin { klucz = węzeł.klucz if węzeł jest liściem, then { usuń węzeł z drzewa } UsunProstyPrzypadek(węzeł) else if węzeł.lewePoddrzewo <> nil then niech x oznacza skrajnie prawy węzeł w poddrzewie węzeł.lewePoddrzewo wezel.klucz:=x.klucz; UsunProstyPrzypadek(x); 'else analogiczne postępowanie dla węzeł.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(węzeł) poddrzewo:=nil; ojciec:=węzeł.ojciec; if (węzeł.lewePoddrzewo) then poddrzewo:=węzeł.lewePoddrzewo; else poddrzewo:=węzeł.prawePoddrzewo; if (ojciec=nil) then korzen:=poddrzewo; else if ojciec.lewePoddrzewo=węzeł then { węzeł jest lewym synem } ojciec.lewePoddrzewo:=poddrzewo; else { węzeł jest prawym synem } ojciec.prawePoddrzewo:=poddrzewo;
Wszystkie podane operacje mają pesymistyczny koszt gdzie oznacza wysokość drzewa. Niestety w najgorszym 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 wszystkie 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.wartość=x) then return element istnieje l:=l.nast; end; return brak elementu end;
Usuwanie elementu z tablicy jest bardzo podobne do wyszukiwania, i również w pesymistycznym przypadku wymaga sprawdzenia całej listy.
procedure Usuń(x); begin l:=A[h(x)];p:=nil; while (l!=nil) do begin if (l.wartość=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