Jeszcze inne spojrzenieNuta na dziśhttps://www.youtube.com/watch?v=UdPOCQGYwrkW strukturze elementu drzewa w pierwotnej wersji jako link w lewo oraz link w prawo występują typy wskaźnikowe (jako informacje o charakterze adresu w pamięci operacyjnej). Co stoi na przeszkodzie interpretować je jako informacje o charakterze adresowym w zewnętrznym pliku. Dane o charakterze adresowym w systemach 8-bitowych to zwyczajowo dane 16-bitowe. Z pewnością zakres przechowywanej informacji będzie mizerny, więc może by tak 32-bity. Zamiast przechowywać samo drzewo w pamięci operacyjnej może by tak ulokować je w innym miejscu: pamięci zewnętrznej. Z technicznego punktu widzenia nie jest to zbyt skomplikowane. Przykładowym rozwiązaniem może być układ AT45DB161 – jako zasobnik typu DataFlash o pojemności 16-megabitów, czyli 2 megabajty. Taki teren, to całkiem spora piaskownica do zagospodarowania. Można by tam urządzić jakąś małą bazkę danych. Obecnie każdy zagadnięty hasłem „baza danych” wręcz odruchowo odpowie SQL. A może jest inne rozwiązanie? Przestawny własne wyobrażenia na inne tory.
Baza danych to coś takiego, co jest przeznaczone do gromadzenia informacji i do szybkiego wyszukiwania określonych danych w wyniku zaistnienia określonych potrzeb. Jak zaprząc do tego algorytmy drzewiaste? Rozpatrzmy przykład.
Wyobraźmy sobie, że jakiś mikrokontroler (przykładowo ATMEGA2560, któremu zostało dołożone jako zewnętrzna pamięć RAM 32kb pamięci statycznej oraz na SPI „powieszony” wspomniany DataFlash). To środowisko stanowi minimum zasobów. Pomijam kwestie interfejsu komunikacyjnego, bo to leży poza sferą aktualnego zainteresowania.
Przykładowy rekord bazy danych może mieć następującą postać:
tree08-01.png
gdzie każde z tych pól ma przewidzianą określoną liczbę znaków. Z tego wynika dosyć istotna cecha: rekordy takiej bazy danych są rekordami o stałej długości (zajmują powtarzalną stałą wielkość w przestrzeni adresowej).
Patrząc na obszar DataFlash, jako zewnętrzna (dla mikrokontrolera) pamięć musi zawierać w sobie jakąś organizację. Nie będę tu namawiał do implementacji systemu FAT czy jakiegokolwiek innego, niemniej jakieś podstawowe minimum organizacyjne musi zostać zachowane. Patrząc na temat tak całkiem z boku przez pryzmat systemów operacyjnych, to nasza bazka danych w najprostszym przypadku składałaby się z dwóch plików: samej bazki → pliku zawierającego wyłącznie zgromadzone dane oraz z pliku indeksowego → pliku pozwalającego na szybkie wyszukiwanie wymaganych danych (coś jak katalog w bibliotece).
Ponieważ jest jedna przestrzeń na przechowywane dane, to „wszystkie pliki” trzeba połączyć do jednej kupy. Z tego względu wydzielam trzy różne struktury, które za każdym razem muszą był łatwo pozyskiwalne, czyli mieć stały adres w przestrzeni. Pod pojęciem adresu w tym miejscu uznaję ofset w pamięci: jak daleko w stosunku do początku pamięci DataFlash znajduje się te <coś>. W każdym przypadku taki odpowiedni rekord danych będzie zawierał istotne informację z punktu widzenia określonej potrzeby.
Przykładowo: baza danych. Specyfiką tego jest to, że generalnie dane przyrastają. Dodając nowe dane przesuwa się granica umownego końca przestrzeni danych. Jednak czasami zachodzi potrzeba usunięcia czegoś. W takim przypadku, powstaje „dziura” w wypełnieniu pamięci DataFlash. Ponieważ nic nie może się zmarnować, to utworzona jest lista liniowa rekordów z odzysku. Dodając nowy rekord, należy sprawdzić, czy czegoś nie da się odzyskać. Skoro jest lista odzyskowych elementów, to koniecznością staje się przechowywanie wskazania na pierwszy wolny rekord (Wolny rekord → jako 32-bitowy ofset w przestrzeni adresowej DataFlash). W nim (gdzieś już daleko w przestrzeni) zapisany jest jego następnik.
tree08-02a.png
Podobnie, część logicznie związana z indeksem (strukturą drzewa) wymaga swojej części „technicznej” umieszczonej w dobrze znanym miejscu.
tree08-02b.png
Jak w każdym drzewie, tu również jest niezbędne wskazanie na szczyt drzewa. Jest to informacja, która z czasem ulega modyfikacji, toteż niezbędne jest przechowywanie tych danych w „dobrze znanym miejscu” w pamięci DataFlash. Podobnie, może zaistnieć problem odzysku danych. Jeżeli zaistnieje zwolnienie pojedynczego elementu, to by nie generować dziur, konieczne jest notowanie wolnych elementów ze struktury drzewa. Coś takiego było wyżej przedstawione przy okazji tablicowej realizacji drzewa. Samo drzewo może również przechowywać informacje o duplikatach. Dokładnie tej klasy problem był opisany wyżej przy okazji Cross Reference. Zatem w strukturze możliwych danych występuje mały rekord zawierający powiązanie kolejnych rekordów bazy danych o identycznym kluczu oraz wskazanie na kolejny element takiej listy (listy identycznych kluczy). Te minimum informacji właśnie jest pokazane na powyższej ilustracji.
Nowe wymagania oznaczają potrzebę stworzenia nowej struktury elementu drzewa. Obok dotychczas znanych elementów występuje nowy: DataLink. Jest to, jak każda informacja o charakterze link informacją 32-bitową jako ofset w pamięci DataFlash. Ta istotna informacja pozwala powiązać indeks (kartkę w katalogu bibliotecznym z uwzględnieniem ewentualnych duplikatów jako listy liniowej) z danymi bazy danych (wskazaniem na „której półce stoi książka”). Jeżeli rekord danych będzie reprezentowany przez adres w przestrzeni DataFlash (czyli 32-bitowy ofset), to przechowując takie wskazanie w strukturze każdego elementu drzewa, uzyskuje się powiązanie jednych danych z drugimi.
tree08-02c.png
Do kompletu należy dodać jeszcze dane organizujące całość (jako plik).
tree08-02.png
Tu minimum, to przechowywanie położenia końca „pliku”, by w operacjach rozszerzenia takiego specyficznego pliku było wiadomo, gdzie się on kończy.
Operacja dodania nowej informacji do bazy danych oznacza w pierwszej kolejności dopisanie samego rekordu danych. To może skończyć się rozszerzeniem „pliku” lub odzyskaniem jakiejś „dziury” w wyniku wyjęcia jej z listy wolnych miejsc. Finalnie, gdziekolwiek dane zostaną zapisane, informacją zwrotną z tej operacji jest ofset położenia rekordu danych. Mając ten ofset i klucz, realizowane jest dodanie elementu drzewa. Tu również może zaistnieć rozbudowanie drzewa (w wyniku rozszerzenia wielkości „pliku” lub odzysku danych z listy wolnych elementów struktury drzewa). Finalnie może to wyglądać jak na ilustracji (NilConst będzie $FFFFFFFF - 32-bity):
tree08-03.png
Odszukanie informacji (np. do kogo należy numer tel 229555777) teraz sprowadza się do:
- wczytanie porcji organizacyjnej indeksu, która zapisana jest w dobrze znanym miejscu w DataFlash,
- z wczytanej porcji uzyskuje się wskazanie na szczyt drzewa,
- mając szczyt drzewa rekurencyjnie odszukać taki element drzewa, który jest zgodny z poszukiwanym kluczem (wymienionym wyżej numerem telefonu),
- mając element drzewa, pozyskać z niego wskazanie na położenie rekordu danych (DataLink).
W rezultacie całej zadymy posiadamy informację gdzie jest zapisany rekord w DataFlash zawierający to co jest poszukiwane.
Oczywiście ze względu na zewnętrzny charakter pamięci do przechowywania wszystkich informacji, należy pamiętać, że algorytm obsługi drzewa przetwarza dane z „obcej” pamięci.
Pewnemu rozszerzeniu ulega struktura elementu drzewa:
Kod: Zaznacz cały
NodeRecordTypePtr = cardinal ;
BalanceState = ( State1 , State2 , State3 ) ;
NodeRecordType = record
Key : array [ 0 .. <ileś> ] od char ;
Balance : BalanceState ;
DataLink : cardinal ;
LeftLink : NodeRecordTypePtr ;
RightLink : NodeRecordTypePtr ;
DuplLink : cardinal ;
end (* record *) ;
Dotychczasowa operacja poszukiwania danych w strukturze drzewa musi być zmodyfikowana przykładowo do postaci pokazanej poniżej. Pamiętając, że zmienna lokalna Node znajduje się w pamięci RAM a dane ją wypełniające znajdują się w innej przestrzeni (DataFlash), konieczne jest wczytanie obszaru DataFlash spod ofsetu NodeEntry (to jest inny „świat”):
Kod: Zaznacz cały
function SearchKey ( Key : integer ;
NodeEntry : NodeRecordTypePtr ) : NodeRecordTypePtr ;
var
Node : NodeRecordType ;
begin (* SearchKey *)
if NodeEntry = NilConst then
SearchKey := NilConst
else
begin (* 1 *)
Wczytaj_Z_DataFlash ( Node , NodeEntry ) ;
if Node . Key = Key then
SearchKey := NodeEntry
else
if Key < Node . Key then
SearchKey := SearchKey ( Key , Node . LeftLink )
else
SearchKey := SearchKey ( Key , Node . RightLink )
end (* 1 *) ;
end (* SearchKey *) ;
Również identycznym komplikacjom podlegają wszystkie inne operacje. W niektórych przypadkach jest to oczywiste, w niektórych można je przeoczyć na pierwszy rzut oka.
Kod: Zaznacz cały
procedure AddKey ( Key : string ;
DataLink : cardinal ;
var NodeEntry : NodeRecordTypePtr ;
var ChangedLength : boolean ) ;
var
Node1 : NodeRecordTypePtr ;
Node2 : NodeRecordTypePtr ;
Node : NodeRecordType ;
begin (* AddKey *)
if NodeEntry = NilConst then
begin (* 1 *)
NodeEntry := UtworzNowyRekord ( ) ;
ChangedLength := true ;
Node . Key := Key ;
Node . DataLink := DataLink ;
Node . Balance := State2 ;
Node . LeftLink := NilConst ;
Node . RightLink := NilConst ;
Node . DuplLink := NilConst ;
Zapisz_Do_DataFlash ( Node , NodeEntry ) ;
end (* 1 *)
( . . . )
end (* AddKey *) ;
W powyższym kawałku, naturalne jest, że DataFlash został wzbogacona o nowy rekord, więc konieczna jest aktualizacja zawartości DataFlash, tak by dane były konsystentne. Trudniej jest zauważyć, że parametr wywołania NodeEntry, również uległa zmianie. Ta nowa zawartości również musi mieć zwoje odbicie w DataFlash. Jednak ta informacja jest częścią składową elementu drzewa na poprzednim poziomie rekurencji, do którego w danej chwili nie ma dostępu.
Kod: Zaznacz cały
procedure AddKey ( Key : string ;
DataLink : cardinal ;
var NodeEntry : NodeRecordTypePtr ;
var ChangedLength : boolean ) ;
var
Node1 : NodeRecordTypePtr ;
Node2 : NodeRecordTypePtr ;
Node : NodeRecordType ;
SaveLink : cardinal ;
begin (* AddKey *)
if NodeEntry = NilConst then
begin (* 1 *)
NodeEntry := UtworzNowyRekord ( ) ;
( . . . )
end (* 1 *)
else
begin (* 1 *)
Wczytaj_Z_DataFlash ( Node , NodeEntry ) ;
if Key < Node . Key then
begin (* 1 *)
SaveLink := Node . LeftLink ;
AddKey ( Key , DataLink , Node . LeftLink , ChangedLength ) ;
if SaveLink <> Node . LeftLink then
Zapisz_Do_DataFlash ( Node , NodeEntry ) ;
( . . . )
end (* AddKey *) ;
Dostęp ten jest natomiast na innym poziomie rekurencji, jednak w tym kontekście trudno jest określić, czy zaistniała zmiana zawartości pola. Dobrym rozwiązaniem jest zapamiętanie linku przez rekurencyjnym wywołaniem i po wyjściu z rekurencji sprawdzić, czy zaistniała odpowiednia zmiana i dokonać aktualizacji zawartości DataFlash. Również każda modyfikacja współczynnika zrównoważenia drzewa wymaga „odciśnięcia swego piętna” w zawartości DataFlash.
Taki indeks pozwala szybko lokalizować dane mając znany numer telefonu. Naturalnym jest, że zachodziłaby potrzeba znalezienia danych mając jedynie imię i nazwisko. W ten sposób dochodzimy powoli do idei „lasu”. Jeżeli dodawany element drzewa posiada pole wskazujące na położenie danych w przestrzeni DataFlash, to zmieńmy znaczenie tej informacji: niech będzie to wskazaniem na szczyt drzewa. Reasumując, powstaje drzewo drzew. Kiedyś w ramach żartu nazwałem do „lasem” i tak już zostało.
Dodanie kolejnego indeksu wymagałoby również dodania porcji organizacyjnej w „dobrze znanym miejscu”. Drugie drzewo, to również drugi szczyt drzewa, lista rekordów z odzysku (inna długość klucza to inna wielkość struktury rekordu drzewa a to prowadzi do innej wielkości „dziur” po usuniętych elementach w przestrzeni DataFlash).
Obrazkowo może to wyglądać następująco:
tree08-04.png
Teraz wyszukanie wszystkich Kowalskich sprowadza się do „wydruku” całego drzewa (w sensie algorytmu jest to wydruk drzewa, w sensie organizacyjnym jest to wydruk drzewa imion „przypiętego” do drzewa nazwisk).
Te rozwiązania stosowałem w rzeczywistych rozwiązaniach pod koniec lat 80-tych tworząc różne bazy danych z rekordami o stałej długości sortując tysiące rekordów i jakoś to się nie gubiło. Co prawa, to realizacja opierała się o komputery PC a pliki (jako rozdzielone) były w przestrzeni dysków. Czasy się trochę zmieniły, ewolucji ulegają możliwości technologiczne, jednak algorytmy, jako przepis na osiągnięcie celu pozostają takie same.
Na tym zakończę już dywagacje na temat drzew i czas przejść na wyższy level: B-drzewa. To jest „odlotowy” wynalazek.
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.