Algorytmy i struktury danych/Wyszukiwanie: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Walen (dyskusja | edycje)
Walen (dyskusja | edycje)
Nie podano opisu zmian
Linia 11: Linia 11:
== Wyszukiwanie liniowe ==
== Wyszukiwanie liniowe ==


Jeśli nie dysponujemy żadną dodatkową więdzą na temat zbioru, który chcemy
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 wszyskiwanie elementów w drzewie.
Dodatkowe wymaganie dotyczące kluczy, umożliwia nam efektywne wyszukiwanie elementów w drzewie.


   '''function''' Szukaj(wezel, klucz)
   '''function''' Szukaj(węzeł, klucz)
     '''if''' (wezel=='''nil''')  
     '''if''' (węzeł=='''nil''')  
         '''return''' BRAK ELEMENTU
         '''return''' BRAK ELEMENTU
     '''if''' (wezel.klucz=klucz) '''then'''
     '''if''' (węzeł.klucz=klucz) '''then'''
         '''return''' ELEMENT ISTNIEJE
         '''return''' ELEMENT ISTNIEJE
     ''' else if (klucz < wezel.klucz) '''then'''
     ''' else if (klucz < węzeł.klucz) '''then'''
         '''return''' Szukaj(wezel.lewePoddrzewo, klucz)
         '''return''' Szukaj(węzeł.lewePoddrzewo, klucz)
     ''' else if (klucz > wezel.klucz) '''then'''
     ''' else if (klucz > węzeł.klucz) '''then'''
         '''return''' Szukaj(wezel.prawPoddrzewo, klucz)
         '''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(wezel, klucz)
   '''procedure''' Dodaj(węzeł, klucz)
     '''if''' (klucz < wezel.klucz) '''then'''
     '''if''' (klucz < węzeł.klucz) '''then'''
         '''if''' wezel.lewePoddrzewo=nil '''then'''
         '''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 wezel.lewePoddrzewo
             wskaźnik do nowego węzła zapisujemy w węzeł.lewePoddrzewo
         '''else'''
         '''else'''
             Dodaj(wezel.lewePoddrzewo, klucz)
             Dodaj(węzeł.lewePoddrzewo, klucz)
     '''else if (klucz >= wezel.klucz) '''then'''
     '''else if (klucz >= węzeł.klucz) '''then'''
         '''if''' wezel.prawePoddrzewo=nil '''then'''
         '''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 wezel.prawePoddrzewo
             wskaźnik do nowego węzła zapisujemy w węzeł.prawePoddrzewo
         '''else'''
         '''else'''
             Dodaj(wezel.prawePoddrzewo, klucz)
             Dodaj(węzeł.prawePoddrzewo, klucz)
   '''end''';
   '''end''';


Możemu również usuwać wartości z drzewa, niestety ta operacja jest bardziej skomplikowana.
Możemy również usuwać wartości z drzewa, niestety ta operacja jest bardziej skomplikowana.


   '''procedure''' Usun(wezel, klucz)  
   '''procedure''' Usuń(węzeł, klucz)  
     '''if''' (klucz < wezel.klucz) '''then'''
     '''if''' (klucz < węzeł.klucz) '''then'''
         Usun(wezel.lewePoddrzewo, klucz)
         Usuń(węzeł.lewePoddrzewo, klucz)
     '''else if (klucz > wezel.klucz) '''then'''
     '''else if (klucz > węzeł.klucz) '''then'''
         Usun(wezel.prawePoddrzewo, klucz)     
         Usuń(węzeł.prawePoddrzewo, klucz)     
     '''else''' '''begin''' { klucz = wezel.klucz
     '''else''' '''begin''' { klucz = węzeł.klucz
         '''if''' ''wezel'' jest liściem, '''then'''
         '''if''' ''węzeł'' jest liściem, '''then'''
           { usuń '''wezel''' z drzewa }
           { usuń '''węzeł''' z drzewa }
           UsunProstyPrzypadek(wezel)
           UsunProstyPrzypadek(węzeł)
         '''else'''
         '''else'''
           '''if''' wezel.lewePoddrzewo <> nil '''then'''
           '''if''' węzeł.lewePoddrzewo <> nil '''then'''
             niech ''x'' oznacza skrajnie prawy węzeł w poddrzewie wezel.lewePoddrzewo
             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 wezel.prawPoddrzewo  
             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(wezel)
   '''procedure''' UsunProstyPrzypadek(węzeł)
     poddrzewo:=nil;
     poddrzewo:=nil;
     ojciec:=wezel.ojciec;
     ojciec:=węzeł.ojciec;
     '''if''' (wezel.lewePoddrzewo) '''then'''
     '''if''' (węzeł.lewePoddrzewo) '''then'''
       poddrzewo:=wezel.lewePoddrzewo;
       poddrzewo:=węzeł.lewePoddrzewo;
     '''else'''     
     '''else'''     
       poddrzewo:=wezel.prawePoddrzewo;     
       poddrzewo:=węzeł.prawePoddrzewo;     
      
      
     '''if''' (ojeciec=nil) '''then'''
     '''if''' (ojciec=nil) '''then'''
       korzen:=poddrzewo;
       korzen:=poddrzewo;
     '''else''' '''if''' ojciec.lewePoddrzewo=wezel '''then''' { węzeł jest lewym synem }
     '''else''' '''if''' ojciec.lewePoddrzewo=węzeł '''then''' { węzeł jest lewym synem }
       ojciec.lewePoddrzewo:=podrzewo;
       ojciec.lewePoddrzewo:=poddrzewo;
     '''else''' { węzeł jest prawym synem }
     '''else''' { węzeł jest prawym synem }
       ojciec.prawePoddrzewo:=podrzewo;
       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 nagorszym przypadku drzewo może mieć wysokość
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 wszyskie operacje słownikowe (dodaj, usuń, szukaj) wykonać
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.wartosc=x) '''then''' '''return''' element istnieje
       '''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 wyszkiwania, i również w pesymistycznym przypadku wymaga sprawdzenia całej listy.
Usuwanie elementu z tablicy jest bardzo podobne do wyszukiwania, i również w pesymistycznym przypadku wymaga sprawdzenia całej listy.


  '''procedure''' Usun(x);
  '''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.wartosc=x) '''then''' '''begin'''
       '''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 O(n).

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 O(logn) operacji, by odnaleźć poszukiwany element, lub stwierdzić jego brak.

Algorytm utrzymuje zakres [l,,r], 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 O(h) gdzie h oznacza wysokość drzewa. Niestety w najgorszym przypadku drzewo może mieć wysokość nawet O(n) (np. dla ciągu operacji Dodaj (1,2,3,)).

Adresowanie bezpośrednie

W przypadku gdy zbiór który przechowujemy pochodzi z niewielkiego uniwersum (na przykład elementy zbioru to liczby z zakresu 1,,n), możemy wszystkie operacje słownikowe (dodaj, usuń, szukaj) wykonać znacznie szybciej i prościej.

Dla uniwersum 1,,n zbiór możemy reprezentować przez tablicę n-elementową. Początkowo w każdej komórce tablicy wpisujemy wartość false.

  • dodanie elementu i do zbioru, wymaga jedynie ustawienia wartości i-tej komórki na true,
  • analogicznie usunięcie elementu i do zbioru, wymaga ustawienia wartości i-tej komórki na false,
  • sprawdzenie czy element i należy do zbioru wykonujemy przez sprawdzenie stanu i-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 O(n), jednak w praktycznych zastosowaniach ta metoda sprawuje się doskonale.

W tym rozdziale będziemy zakładać, że elementy uniwersum U, to dodatnie liczby całkowite. Dodatkowo zakładamy, że dysponujemy tablicą A[0,,m1].

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ą:

h:U0,,m1

Funkcja ta dla każdego elementu uniwersum przypisuje odpowiednie miejsce w tablicy A. Jeśli |U|>m, to z oczywistych względów znajdą się takie pary różnych elementów x,yU, dla których f(x)=f(y). 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 A wypełniona jest wartościami nil.

procedure Inicjalizacja;
begin
  for i:=0 to m-1 do A[i]:=nil;
end;

Dodanie elementu x do tablicy, wymaga odczytania jego adresu z funkcji haszującej, a następnie dodania go na początek listy A[h(x)]

procedure Dodaj(x);
begin
  dodaj element x na początek listy A[h(x)]
end;

Sprawdzenie czy element x istnieje, wymaga w pesymistycznym przypadku sprawdzenia całej listy A[h(x)]

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

powrót do strony przedmiotu