"Drzewa" i "las"
: niedziela 01 gru 2019, 02:54
Drzewa binarne
Chcąc ocalić od zapomnienia. Garść dywagacji na temat drzew binarnych.https://www.youtube.com/watch?v=2oANV6SQhwI
Jest taki wynalazek, który nazywa się drzewa binarne. Przymiotnik „binarne” symbolizuje to, że każdy element drzewa ma dwóch potomków (synów czy córek). To dosyć wygodna struktura pozwalająca na usystematyzowanie (uporządkowanie) czegoś. Oczywiście zastosowań tego algorytmu można zaproponować całe multum. Bazując na tym algorytmie realizowałem pierwsze rozwiązania z tematu baz danych. Jednak zanim zagłębię się w to zagadnienie, w pierwszej kolejności przedstawię kilka dywagacji i rozważań na temat algorytmu. Te eksperymenty będą oparte o zmienne wskaźnikowe lokowane w pamięci programu. Struktura elementu drzewa (Node) w najprostszym rozwiązaniu pokazana jest na rysunku:gdzie:
Z drzewem związany jest element będący szczytem (początkiem) drzewa. Czasem można napotkać określenie: korzeń. To trochę przypomina dywagację filozoficzną, gdyż korzeń to z reguły rośnie w ziemi a szczyt drzewa ucieka „do nieba” w kosmos. Jakkolwiek by to nazywać, znaczenie tego jest takie, że wskazuje na pierwszy element struktury drzewa.
Program ilustrujący jest w Lazarus (Pascal).
Rozpatrzmy taki typ:i zmienną:Podstawienie:oznacza, że drzewo symbolizowane zmienną TopTree jest puste (nie zawiera żadnego elementu). Wszelkie operacje na drzewach najprościej implementują się w oparciu o algorytmy rekurencyjne. Rozpatrzmy procedurę budującą drzewo:Procedura dodaje nowy klucz (reprezentowany przez parametr Key) do drzewa (reprezentowanego przez parametr NodeEntry). Istotnym elementem jest to, że wskaźnik do drzewa musi być parametrem z zaklęciem var, procedura dodania elementu drzewa dodając modyfikuje wskaźniki, które muszą być „widoczne” na zewnątrz. Algorytm jest rekurencyjny a jego realizacja jest operacją poszukiwania z ewentualnym wstawieniem nowego elementu. Nowy element zawsze doda się na końcu gałęzi drzewa, czyli rekurencyjnie należy dokręcić się do takiego miejsca, gdzie bieżący wskaźnik elementu drzewa jest nil. Jeżeli bieżący element istnieje (jest różny od nil), to należy odpowiednio (w ogólnym przypadku w wyniku zastosowania funkcji porządkującej) przeszukać lewe (reprezentowane przez dziecko LeftLink) lub prawe (reprezentowane przez dziecko RightLink) poddrzewo. Tu, dla uproszczenia nie jest rozpatrywana kwestia duplikatów, dodania elementu już istniejącego (gdzie funkcja porządkująca da wynik: równy, o czym będzie później).
Rozpatrzmy ciąg wywołań:Po wstępnym podstawieniu TopTree:=nil drzewo jest puste i pierwsze rekurencyjne wywołanie AddKey ( 8 , TopTree ) ; utworzy element. Utworzony element „doczepi się” do zmiennej TopTree (parametr w wywołaniu funkcji dodania klucza jest typu var).Po pierwszym wywołaniu będzie:Kolejne wywołanie: AddKey ( 4 , TopTree ) ; zauważy, że nowododawany klucz (4) jest mniejszy od aktualnego (8), więc dojdzie do rekurencyjnego wywołania z potomkiem LeftLink. Ponieważ ten jest równy nil, utworzy się nowy element, który doczepi się jako potomek LeftLink. Po akcji będzie:Po kolejnej akcji, drzewo przyjmie postać:Po kolejnej serii wywołań:Drzewo wygląda następująco:Ostatecznie:Tu muszę się przyznać, że przykład dobrany jest tendencyjnie, by wszystko wyszło „ładnie”. Niestety nie zawsze jest tak pięknie i cudownie, co wymaga trochę trudu by doprowadzić do takiego stanu (będzie o tym dalej).
Rozpatrzmy inny ciąg wywołań procedur realizujących budowanie drzewa.Doprowadzi on do następującego efektu:W tym wypadku doświadczymy pewnej przykrości: drzewo binarne zredukowało się do listy liniowej. Ma to dosyć istotny wpływ na optymalność rozwiązania pod każdym względem:
Poszukiwanie elementu w drzewie:Funkcja poszukująca zwraca wskaźnik do znalezionego elementu (jeżeli został znaleziony). Jeżeli aktualny wskaźnik jest pusty (NodeEntry = nil), to oznacza, że poszukiwany element nie występuje w drzewie (dotarliśmy do granic galaktyki i nie natrafiliśmy na poszukiwany element). W przeciwnym wypadku (element drzewa istnieje), to są dwa wyjścia: albo został znaleziony (to zwracany jest jako wynik funkcji wskaźnik do tego elementu) albo nie: należy odpowiednio przeszukać lewe lub prawe poddrzewo. Zagadnienie ma charakter fraktalny: z każdego punktu widzenia wygląda identycznie.
Równie prosto jest realizowana operacja usunięcia całego drzewa.Rekurencyjny algorytm, z każdego punktu można interpretować następująco: usuń lewe poddrzewo, usuń prawe poddrzewo i usuń się sam.
Wydruk całego drzewa: nic prostszego:sprowadza się do: wydrukuj lewe poddrzewo (elementy uporządkowane młodsze), wydrukuj siebie i wydrukuj prawe poddrzewo (elementy uporządkowane starsze). W wyniku mamy uporządkowany ciąg elementów bez względu na kolejność wkładania. Wkładanie to ważna rzecz: w różny sposób tworzą się dzieci (jak było widać wyżej). Tu taka ciekawostka, zamieniamy kolejność rekurencyjnych wywołań: w pierwszej kolejności link w prawo, na koniec rekurencyjne wywołanie z linkiem w lewo. Uzyskamy wynik uporządkowany w przeciwną stronę: jak niewielka zmiana w algorytmie może mieć daleko idące skutki finalne.
Z podstawowych operacji pozostało usunięcie określonego elementu z drzewa, ale to już w innym odcinku.
Dla tropicieli (Lazarus):
Chcąc ocalić od zapomnienia. Garść dywagacji na temat drzew binarnych.https://www.youtube.com/watch?v=2oANV6SQhwI
Jest taki wynalazek, który nazywa się drzewa binarne. Przymiotnik „binarne” symbolizuje to, że każdy element drzewa ma dwóch potomków (synów czy córek). To dosyć wygodna struktura pozwalająca na usystematyzowanie (uporządkowanie) czegoś. Oczywiście zastosowań tego algorytmu można zaproponować całe multum. Bazując na tym algorytmie realizowałem pierwsze rozwiązania z tematu baz danych. Jednak zanim zagłębię się w to zagadnienie, w pierwszej kolejności przedstawię kilka dywagacji i rozważań na temat algorytmu. Te eksperymenty będą oparte o zmienne wskaźnikowe lokowane w pamięci programu. Struktura elementu drzewa (Node) w najprostszym rozwiązaniu pokazana jest na rysunku:gdzie:
- Klucz – jest porządkowaną informacją, w przykładach jest to liczba, w ogólnym przypadku może być to dowolna informacja, dla której można określić funkcję komparującą, funkcję do porównywania treści kluczy, która daje wynik: równy, mniejszy lub większy.
- Link w lewo – jest wskazaniem na kolejny element drzewa, który zawiera klucze mniejsze od aktualnego (zawartego w określonym elemencie drzewa),
- Link w prawo – jest wskazaniem do kolejny element drzewa, który zawiera klucze większe od aktualnego.
Z drzewem związany jest element będący szczytem (początkiem) drzewa. Czasem można napotkać określenie: korzeń. To trochę przypomina dywagację filozoficzną, gdyż korzeń to z reguły rośnie w ziemi a szczyt drzewa ucieka „do nieba” w kosmos. Jakkolwiek by to nazywać, znaczenie tego jest takie, że wskazuje na pierwszy element struktury drzewa.
Program ilustrujący jest w Lazarus (Pascal).
Rozpatrzmy taki typ:
Kod: Zaznacz cały
type
NodeRecordTypePtr = ^ NodeRecordType ;
NodeRecordType = record
Key : integer ;
LeftLink : NodeRecordTypePtr ;
RightLink : NodeRecordTypePtr ;
end (* record *) ;Kod: Zaznacz cały
var
TopTree : NodeRecordTypePtr ; Kod: Zaznacz cały
TopTree := nil ;Kod: Zaznacz cały
procedure AddKey ( Key : integer ;
var NodeEntry : NodeRecordTypePtr ) ;
begin (* AddKey *)
if NodeEntry = nil then
begin (* 1 *)
New ( NodeEntry ) ;
NodeEntry ^ . Key := Key ;
NodeEntry ^ . LeftLink := nil ;
NodeEntry ^ . RightLink := nil ;
end (* 1 *)
else
if Key < NodeEntry ^ . Key then
AddKey ( Key , NodeEntry ^ . LeftLink )
else
AddKey ( Key , NodeEntry ^ . RightLink )
end (* AddKey *) ;Rozpatrzmy ciąg wywołań:
Kod: Zaznacz cały
TopTree := nil ;
AddKey ( 8 , TopTree ) ;
AddKey ( 4 , TopTree ) ;
AddKey ( 12 , TopTree ) ;
AddKey ( 2 , TopTree ) ;
AddKey ( 6 , TopTree ) ;
AddKey ( 10 , TopTree ) ;
AddKey ( 14 , TopTree ) ;
AddKey ( 1 , TopTree ) ;
AddKey ( 3 , TopTree ) ;
AddKey ( 5 , TopTree ) ;
AddKey ( 7 , TopTree ) ;
AddKey ( 9 , TopTree ) ;
AddKey ( 11 , TopTree ) ;
AddKey ( 13 , TopTree ) ;
AddKey ( 15 , TopTree ) ;Kod: Zaznacz cały
AddKey ( 2 , TopTree ) ;
AddKey ( 6 , TopTree ) ;
AddKey ( 10 , TopTree ) ;
AddKey ( 14 , TopTree ) ;Rozpatrzmy inny ciąg wywołań procedur realizujących budowanie drzewa.
Kod: Zaznacz cały
TopTree := nil ;
AddKey ( 7 , TopTree ) ;
AddKey ( 6 , TopTree ) ;
AddKey ( 5 , TopTree ) ;
AddKey ( 4 , TopTree ) ;
AddKey ( 3 , TopTree ) ;
AddKey ( 2 , TopTree ) ;
AddKey ( 1 , TopTree ) ;- algorytm musi się głębiej wkręcać rekurencyjnie, co odbija się na wielkości wymaganego stosu,
- koszt poszukiwania elementu jest proporcjonalny do liczby elementów (w przypadku „pięknego drzewa”, koszt jest proporcjonalny do logarytmu przy podstawie 2 z liczby elementów).
Poszukiwanie elementu w drzewie:
Kod: Zaznacz cały
function SearchKey ( Key : integer ;
NodeEntry : NodeRecordTypePtr ) : NodeRecordTypePtr ;
begin (* SearchKey *)
if NodeEntry = nil then
SearchKey := nil
else
if NodeEntry ^ . Key = Key then
SearchKey := NodeEntry
else
if Key < NodeEntry ^ . Key then
SearchKey := SearchKey ( Key , NodeEntry ^ . LeftLink )
else
SearchKey := SearchKey ( Key , NodeEntry ^ . RightLink )
end (* SearchKey *) ;Równie prosto jest realizowana operacja usunięcia całego drzewa.
Kod: Zaznacz cały
procedure DisposeTree ( var NodeEntry : NodeRecordTypePtr ) ;
begin (* DisposeTree *)
if NodeEntry <> nil then
begin (* 1 *)
DisposeTree ( NodeEntry ^ . LeftLink ) ;
DisposeTree ( NodeEntry ^ . RightLink ) ;
Dispose ( NodeEntry ) ;
end (* 1 *)
end (* DisposeTree *) ;Wydruk całego drzewa: nic prostszego:
Kod: Zaznacz cały
procedure PrintContent ( NodeEntry : NodeRecordTypePtr ) ;
begin (* PrintContent *)
if NodeEntry <> nil then
begin (* 1 *)
PrintContent ( NodeEntry ^ . LeftLink ) ;
Line := 'Klucz: ' + IntToStr ( NodeEntry ^ . Key ) ; <---- w znaczeniu
Memo . Lines . Add ( Line ) ; <---- drukuj siebie
PrintContent ( NodeEntry ^ . RightLink ) ;
end (* 1 *) ;
end (* PrintContent *) ;Kod: Zaznacz cały
procedure PrintContent ( NodeEntry : NodeRecordTypePtr ) ;
begin (* PrintContent *)
if NodeEntry <> nil then
begin (* 1 *)
PrintContent ( NodeEntry ^ . RightLink ) ;
Line := 'Klucz: ' + IntToStr ( NodeEntry ^ . Key ) ;
Memo . Lines . Add ( Line ) ;
PrintContent ( NodeEntry ^ . LeftLink ) ;
end (* 1 *) ;
end (* PrintContent *) ;Z podstawowych operacji pozostało usunięcie określonego elementu z drzewa, ale to już w innym odcinku.
Dla tropicieli (Lazarus):