background image

 

- 1 -

  

B

INARNE 

D

RZEWA 

POSZUKIWAŃ 

 

Na podstawie: N. Wirth, „Algorytmy + struktury danych =programy”, WNT, 

 

Eleganckim i efektywnym sposobem definiowania bardziej złożonych struktur 

danych jest rekurencja. Zdefiniujmy zatem strukturę drzewiastą następująco: 

Struktura drzewiasta o typie podstawowym T jest albo 

1)  Strukturą pustą, albo 

2)  Węzłem  typu  T  ze  skończoną  liczbą  dowiązanych  rozłącznych  struktur 

drzewiastych o typie podstawowym T, nazywanych poddrzewami

 

Przykłady drzew: 

·  Struktura plików systemu operacyjnego 
·  Książka, rozdziały, podrozdziały, sekcje, paragrafy, itd. 
·  Drzewo genealogiczne 
·  Menu telefonu komórkowego 
·  Drzewo opisujące wyrażenie arytmetyczne 

 

Reprezentacja drzew 

Jest  wiele  sposobów  reprezentacji  struktur  drzewiastych.  Na  przykład  niech 

typ podstawowy T obejmuje litery; odpowiednia struktura jest przedstawiona 

na kilka sposobów na rys. 1. Wszystkie reprezentacje mają taką samą strukturę 

i  dlatego  są  równoważne.  Spośród  nich  graf  najwyraźniej  ilustruje  relacje 

powiązań.  Charakter  tych  powiązań  uzasadnia  wybór  nazwy  struktury  - 

„drzewo".  Rzecz  dziwna,  zwykle  rysuje  się  drzewo  odwrócone  lub  -  wyrażając 

ten fakt inaczej - uwidacznia się korzenie drzewa. 

background image

 

- 2 -

 

Rysunek 1

 

background image

 

- 3 -

Terminologia 

 

Korzeniem  drzewa  nazywamy  zwykle  najwyższy  węzeł  (A).  Drzewem 

uporządkowanym  jest  drzewo,  w  którym  gałęzie  każdego  węzła  są 

uporządkowane.  Węzeł  y,  który  znajduje  się  bezpośrednio  poniżej  węzła  x

nazywamy (bezpośrednim) potomkiem x. Jeżeli znajduje się na poziomie 

i,  to  mówimy,  że  y  znajduje  się  na  poziomie  i  +  1.  I  odwrotnie,  węzeł  

nazywany  (bezpośrednim)  przodkiem  y.  Poziom  korzenia  drzewa 

definiujemy  jako  1.  Maksymalny  poziom  wszystkich  elementów  drzewa 

nazywamy  jego  głębokością  lub  wysokością.  Element  nie  mający 

potomków  nazywamy  elementem  końcowym  lub  liściem.  Element  nie 

będący  liściem  nazywamy  węzłem  wewnętrznym.  Liczbę  (bezpośrednich) 

potomków  wewnętrznego  węzła  nazywamy  jego  stopniem.  Maksymalny 

stopień  wszystkich  węzłów  jest  stopniem  drzewa.  Liczbę  gałęzi  lub  krawędzi, 

przez  które  należy  przejść  od  korzenia  do  węzła  x,  nazywamy  długością 

ścieżki (lub drogi) x. Długość ścieżki korzenia jest równa 1, jego bezpośredni 

potomek  ma  długość  ścieżki  równą  2  itd.  Ogólnie,  długość  ścieżki  węzła  na 

poziomie i jest równa i. Długość ścieżki drzewa definiujemy jako sumę długości 

ścieżek  wszystkich  jego  składowych.  Nazywamy  ją  także  długością  ścieżki 

wewnętrznej. Na przykład długość ścieżki wewnętrznej drzewa z rys. 1 równa 

jest 52. 

Drzewo  jest  doskonale  zrównoważone  (dokładnie  wyważone),  jeżeli  dla 

każdego węzła liczby węzłów w jego lewym i prawym poddrzewie różnią się co 

najwyżej o 1. 

 

 

 

 

 

 

 

background image

 

- 4 -

Drzewa binarne 

 

Drzewa uporządkowane o stopniu 2 okazują się szczególnie ważne. Nazywane 

są drzewami binarnymi. Uporządkowane drzewo binarne definiujemy jako 

skończony  zbiór  elementów  (węzłów),  który  jest  albo  pusty,  albo  zawiera 

korzeń (węzeł) z dwoma rozłącznymi binarnymi drzewami zwanymi lewym 

i prawym poddrzewem korzenia. 

Każdy  węzeł  ma  co  najwyżej  2  następniki,  które  potocznie  nazywane  są 

„lewym”  i  „prawym”.  Dodatkowo,  jeśli  wiadomo,  że  dla  każdego  węzła  t

i

 

wszystkie  klucze  z  lewego  poddrzewa  węzła  t

i

  są  mniejsze  od  klucza  węzła  t

i

a  klucze  z  prawego  poddrzewa  są  od  niego  większe,  to  mamy  do  czynienia 

wtedy z drzewem  poszukiwań. W drzewie takim można znaleźć określony 

klucz,  posuwając  się  wzdłuż  ścieżki  poszukiwania  –  począwszy  od  korzenia  – 

i  przechodząc  do  lewego  lub  prawego  poddrzewa  danego  węzła  w  zależności 

tylko od wartości klucza w tym węźle. 

 

 

Rysunek 2

 

 

Drzewo  w  swym  korzeniu  ma  wartość  10.  Zauważmy,  że  wszystkie  wartości 

leżące  na  jego  lewych  gałązkach  są  od  10  mniejsze,  a  na  prawych  większe. 

Możemy przyjrzeć się np. poddrzewu o wartości korzenia 7. Znów sytuacja się 

powtarza  -  wszystko,  co  po  lewej  stronie  jest  mniejsze,  a  to  co  po  prawej  - 

większe. 

 

background image

 

- 5 -

Przeglądanie drzewa 

 

Czynnością  powszechnie  wykonywaną  na  strukturze  drzewiastej  jest 

odwiedzanie  wszystkich  węzłów,  nazywane  zwykle  przeglądaniem  drzewa 

(przechodzeniem  drzewa).  Jeśli  rozważymy  to  zadanie  jako  proces 

sekwencyjny,  to  odwiedzanie  pojedynczych  węzłów  odbywa  się  w  pewnym 

porządku i można uważać, że węzły zostały ułożone wzdłuż linii. 

Istnieją  trzy  podstawowe  uporządkowania  wynikające  w  sposób  naturalny  ze 

struktury  drzew.  Podobnie  jak  samą  strukturę  drzewiastą  można  je  zręcznie 

wyrazić  za  pomocą  pojęć  rekurencyjnych.  Powołując  się  na  drzewo  binarne  z 

rys.  3,  w  którym  K  oznacza  korzeń,  A  i  B  zaś  –  lewe  i  prawe  poddrzewa, 

wyróżniamy trzy porządki: 

1)  preorder (wzdłużny): K, A, B (odwiedzamy korzeń przed poddrzewami); 

2)  inorder (poprzeczny): A, K, B; 

3)  postorder (wsteczny): A, B, K (odwiedzamy korzeń po poddrzewach). 

 

 

Rysunek 3

 

 

 

 

 

 

background image

 

- 6 -

Implementacja 

 

Przyjmujemy następujące definicje typów danych: 

 

type 

   wsk = ^wezel; 

   wezel = record 
              klucz : integer; 

              licznik : integer; 

              lewe, prawe : wsk; 
           end; 

 
Procedura Drukowanie odpowiada za wydruk powstałego drzewa na ekranie. 

Zastosowano  metodę  inorder,  dzięki  czemu  wydrukowane  elementy  są 

uporządkowane rosnąco. 

 

procedure Drukowanie (w : wsk; l : integer); 
var 

   i : integer; 

begin 
   if w <> nil then 

      with w^ do 

         begin 
            Drukowanie (lewe, l+1); 

            for i := 1 to l do 

               write (' '); 
            writeln (klucz); 

            Drukowanie (prawe, l+1); 

         end 
end; 

 
 

Procedura Szukaj_i_Wstaw szuka w drzewie elementu x, jeśli taki element 

się  znajduje,  wtedy  zwiększa  licznik  o  1,  a  jeśli  nie,  wtedy  element  jest 

dodawany do drzewa. 

Poczynając  od  pustego  drzewa,  szukamy  w  drzewie  danego  słowa.  Po  jego 

znalezieniu  zwiększa  się  licznik  wystąpień.  W  przeciwnym  razie  jest  ono 

wstawiane  jako  nowe  słowo  (z  licznikiem  równym  1).  Rozważmy  na  przykład 

drzewo binarne z rys. 4 i wstawienie słowa „Paweł”. Wynik oznaczono liniami 

przerywanymi na tym samym rysunku. 

background image

 

- 7 -

 

Rysunek 4

 

 
Proces  przeszukiwania  jest  sformułowany  w  postaci  procedury  rekurencyjnej. 

Zwróćmy uwagę, że parametr p jest przekazywany przez zmienną, a nie przez 

wartość.  Jest  to  istotne,  gdyż  przy  wstawianiu  zmiennej  mającej  uprzednio 

wartość nil musi być przypisana nowa wartość wskaźnika. 

Korzystając z ciągu wejściowych 21 liczb: 

8 9 11 15 19 20 21 7 3 2 1 5 6 4 13 14 10 12 17 16 18 

program daje w wyniku drzewo binarne pokazane na rys. 5. 

background image

 

- 8 -

 

Rysunek 5

 

 

procedure Szukaj_i_Wstaw (x : integer; var p : wsk); 
begin 

   if p = nil then 
      begin 

         new (p); 

         with p^ do 
            begin 

               klucz := x; 

               licznik := 1; 
               lewe := nil; 

               prawe := nil; 

            end; 
         writeln ('Elementu nie bylo w drzewie. Wstawiono nowy wezel.'); 

      end 

   else 
      if x < p^.klucz then 

         Szukaj_i_Wstaw (x, p^.lewe) 

      else if x > p^.klucz then 
         Szukaj_i_Wstaw (x, p^.prawe) 

      else 

         begin 
            p^.licznik := p^.licznik + 1; 

            writeln ('Element zjadowal sie w drzewie. Zwiekszono licznik o 1.'); 

         end; 
end; 

 

background image

 

- 9 -

Przejdźmy  teraz  do  problemu  odwrotnego  względem  wstawiania,  tj.  do 

usuwania.  Zadaniem  naszym  jest  zdefiniowanie  algorytmu  usuwania  węzła  o 

kluczu x z drzewa z uporządkowanymi kluczami. Niestety, usunięcie elementu 

nie jest na ogół takie proste jak wstawienie. Jest to łatwa operacja wtedy, gdy 

usuwany  element  jest  węzłem  końcowym  lub  ma  tylko  jednego  potomka. 

Trudności pojawiają się przy usuwaniu elementu mającego dwóch potomków, 

ponieważ jednym wskaźnikiem nie możemy wskazać dwóch kierunków. W tej 

sytuacji  usuwany  element  musi  być  zastąpiony  przez  skrajny  prawy  element 

lewego poddrzewa lub skrajny lewy węzeł prawego poddrzewa, z których każdy 

ma  co  najwyżej  jednego  potomka.  Szczegóły  algorytmu  są  zawarte  w 

rekurencyjnej procedurze Usuń. Rozróżnia się w niej trzy przypadki: 

1)  Brak składowej o kluczu równym x. 

2)  Składowa o kluczu x ma co najwyżej jednego potomka. 

3)  Składowa o kluczu x ma dwóch potomków. 

 

Pomocnicza  procedura  rekurencyjna  Us  jest  wywoływana  tylko  w  trzecim 

przypadku. „Schodzi” ona wzdłuż skrajnej prawej krawędzi lewego poddrzewa 

elementu  q^,  który  należy  usunąć,  a  następnie  wymienia  związaną  z  q^ 

informację  (klucz  i  licznik)  na  odpowiadające  wartości  skrajnie  prawego 

składnika r^ lewego poddrzewa, po czym r^ może być zwolniony. 

Aby  zilustrować  funkcjonowanie  procedury,  odwołamy  się  do  rys.  6.  Mamy 

dane  drzewo  (a),  następnie  usuwamy  kolejno  węzły  o  kluczach  13,  15,  5,  10

Drzewo powstałe w wyniku tej operacji pokazano na rysunku. 

background image

 

- 10 -

 

Rysunek 6

 

 

 

procedure Usun (x : integer; var p : wsk); 

var 
   q : wsk; 

 

   procedure Us (var r : wsk); { Procedura pomocnicza Us } 
   begin 

      if r^.prawe <> nil then 

         Us (r^.prawe) 
      else 

         begin 

            q^.klucz := r^.klucz; 
            q^.licznik := r^.licznik; 

            q:= r; 

            r:= r^.lewe; 
         end; 

   end; {--- Us -} 

 
begin 

   if p = nil then 

      writeln ('Brak slowa w drzewie.') 
   else 

      if x < p^.klucz then 

         Usun (x, p^.lewe) 
      else 

         if x > p^.klucz then 

            Usun (x, p^.prawe) 
         else 

            begin { Usun p^ } 

               q := p; 
               if q^.prawe = nil then 

                  p := q^.lewe 

               else 
                  if q^.lewe = nil then 

                     p := q^.prawe 
                  else Us (q^.lewe); 

            end 

end; 

background image

 

- 11 -

Program drzewo.pas 
 

program Drzewo_Binarne; 
 
uses 
   Crt; 
 
type 
   wsk = ^wezel; 
   wezel = record 

              klucz : integer; 
              licznik : integer; 
              lewe, prawe : wsk; 
           end; 
 
var 
   korzen : wsk; 

   k : integer; 
   co : char; 
 
 
procedure Drukowanie (w : wsk; l : integer); 
var 
   i : integer; 
begin 

   if w <> nil then 
      with w^ do 
         begin 
            Drukowanie (lewe, l+1); 
            for i := 1 to l do 
               write (' '); 
            writeln (klucz); 
            Drukowanie (prawe, l+1); 

         end 
end; {--- Drukowanie -} 
 
 
procedure Szukaj_i_Wstaw (x : integer; var p : wsk); 
{ Procedura szuka w drzewie elementu x, jesli taki element sie 
znajduje,        } 
{ wtedy zwieksza licznik o 1, a jesli nie, wtedy elemet jest dodawany 

do drzewa } 
begin 
   if p = nil then 
      begin 
         new (p); 
         with p^ do 
            begin 
               klucz := x; 

               licznik := 1; 
               lewe := nil; 
               prawe := nil; 
            end; 
         writeln ('Elementu nie bylo w drzewie. Wstawiono nowy 
wezel.'); 
      end 
   else 

      if x < p^.klucz then 
         Szukaj_i_Wstaw (x, p^.lewe) 
      else if x > p^.klucz then 
         Szukaj_i_Wstaw (x, p^.prawe) 
      else 
         begin 
            p^.licznik := p^.licznik + 1; 
            writeln ('Element zjadowal sie w drzewie. Zwiekszono licznik 

o 1.'); 
         end; 
end; {--- Szukaj_i_Wstaw -} 
 
 
procedure Usun (x : integer; var p : wsk); 
var 
   q : wsk; 

 
   procedure Us (var r : wsk); { Procedura pomocnicza Us } 
   begin 
      if r^.prawe <> nil then 
         Us (r^.prawe) 
      else 
         begin 
            q^.klucz := r^.klucz; 

            q^.licznik := r^.licznik; 
            q:= r; 
            r:= r^.lewe; 
         end; 
   end; {--- Us -} 
 
begin 
   if p = nil then 

      writeln ('Brak slowa w drzewie.') 
   else 
      if x < p^.klucz then 
         Usun (x, p^.lewe) 
      else 
         if x > p^.klucz then 
            Usun (x, p^.prawe) 

         else 
            begin { Usun p^ } 
               q := p; 
               if q^.prawe = nil then 
                  p := q^.lewe 
               else 
                  if q^.lewe = nil then 
                     p := q^.prawe 

                  else Us (q^.lewe); 
            end 
end; {--- Usun -} 
 
 
begin { Main } 
 
   korzen := nil; { Utworzenie pustego drzewa } 

 
   repeat 
      ClrScr; 
 
      writeln ('Wybierz operacje: '); 
      writeln ('1 - Drukowanie drzewa'); 
      writeln ('2 - Szukaj i Wstaw'); 
      writeln ('3 - Usun z drzewa'); 

      writeln ('4 - Aktualizacja'); 
      writeln; 
      writeln ('0 - Koniec programu'); 
      writeln; 
 
      co := readkey; 
 
      case co of 

         '1' : begin 
                  Drukowanie (korzen, 3); 
                  write ('Nacisnij Enter, aby kontynuowac'); 
                  readln; 
               end; 
         '2' : begin 
                  write ('Podaj wartosc k: '); 
                  readln (k); 

                  Szukaj_i_Wstaw (k, korzen); 
 

 

 

 

  write ('Nacisnij Enter, 

aby kontynuowac'); 
                  readln; 
               end; 
         '3' : begin 
                  write ('Podaj wartosc k: '); 
                  readln (k); 

                  Usun (k, korzen); 
               end; 
         '4' : begin 
                  write ('Podaj stara wartosc: '); 
                  readln (k); 
                  Usun (k, korzen); 
                  write ('Podaj nowa wartosc: '); 
                  readln (k); 

                  Szukaj_i_Wstaw (k, korzen); 
               end; 
      end; 
   until co = '0'; 
end. {--- Main -}