AISD str. A 1 Algorytmy i Struktury Danych Literatura S. Sengupta, C. Ph. Korobkin, "C++ Object-Oriented Data Structures", Springer-Verlag, 1994. N. Wirth.”Algorytmy + struktury danych = programy” WNT Warszawa 1980, B. Stroustrup, "The C++ Programming Language", Second Edition, Addison Wesley, 1991, W. Iszkowski, "Struktury i typy danych", Wydawnictwa Politechniki Warszawskiej, Warszawa, 1990. Program wykładu 1. Algorytmy i struktury danych w ujęciu praktyczno-teoretycznym. 2. Liniowe struktury danych. 3. Sortowanie i wyszukiwanie. 4. Drzewa. 5. Rekursja. 6. Grafy. ALGORYTMY I STRUKTURY DANYCH W UJĘCIU PRAKTYCZNO- TEORETYCZNYM ALGORYTMY I STRUKTURY DANYCH, A TYPY DANYCH Dwa fundamentalne składniki programów to: struktury danych (wartości danych zorganizowane w pewien sposób umożliwiający dostęp do nich) algorytmy (opis zasady przetwrzania wartości o ich organizacji) Przy tworzeniu złożonych programów struktury i algorytmy grupuje się dla uzyskania większej przejrzystości, a grupy te nazywa się modułami, typami danych, klasami, czy definicjami typów danych. Pojęcie typów danych pozwala umożliwia mówienie o oprogramowaniu w sposób bardziej abstrakcyjny niż przedstawianie szczegółowego sposobu reprezentowania danych i szczegółów działania algorytmów. Pojęcie typów danych i w szczególności abstrakcyjnych typów danych było promowane przez wielu autorów w latach 70-tych i zostało zaadoptowane przez praktyków programowania w latach 80-tych. Def. Specyfikacja (opis nieformalny lub prezentacja algebraiczna lub kod programu): Specyfikacją (formalną) jest opis (formalny) systemu typów danych, to jest opis rodzajów, rodzajów argumentów i wyników operacji oraz opis zależności pomiędzy wartościami argumentów, a wartościami wyników operacji.
111
Embed
Algorytmy i Struktury Danych - carme.pld-linux.orgcarme.pld-linux.org/~evil/varia/informatyka/semestr_2/Algorytmy i... · Algorytmy i Struktury Danych Literatura S. Sengupta, C. Ph.
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
AISD str. A
1
Algorytmy i Struktury Danych
LiteraturaS. Sengupta, C. Ph. Korobkin, "C++ Object-Oriented Data Structures", Springer-Verlag, 1994.N. Wirth.”Algorytmy + struktury danych = programy” WNT Warszawa 1980,B. Stroustrup, "The C++ Programming Language", Second Edition, Addison Wesley, 1991,W. Iszkowski, "Struktury i typy danych", Wydawnictwa Politechniki Warszawskiej, Warszawa,1990.
Program wykładu
1. Algorytmy i struktury danych w ujęciu praktyczno-teoretycznym.2. Liniowe struktury danych.3. Sortowanie i wyszukiwanie.4. Drzewa.5. Rekursja.6. Grafy.
ALGORYTMY I STRUKTURY DANYCH W UJĘCIU PRAKTYCZNO-TEORETYCZNYM
ALGORYTMY I STRUKTURY DANYCH, A TYPY DANYCH
Dwa fundamentalne składniki programów to:struktury danych (wartości danych zorganizowane w pewien sposób
umożliwiający dostęp do nich)algorytmy (opis zasady przetwrzania wartości o ich organizacji)
Przy tworzeniu złożonych programów struktury i algorytmy grupuje się dla uzyskania większejprzejrzystości, a grupy te nazywa się modułami, typami danych, klasami, czy definicjami typówdanych.
Pojęcie typów danych pozwala umożliwia mówienie o oprogramowaniu w sposób bardziejabstrakcyjny niż przedstawianie szczegółowego sposobu reprezentowania danych i szczegółówdziałania algorytmów. Pojęcie typów danych i w szczególności abstrakcyjnych typów danychbyło promowane przez wielu autorów w latach 70-tych i zostało zaadoptowane przez praktykówprogramowania w latach 80-tych.
Def. Specyfikacja (opis nieformalny lub prezentacja algebraiczna lub kod programu):Specyfikacją (formalną) jest opis (formalny) systemu typów danych, to jest opis rodzajów,rodzajów argumentów i wyników operacji oraz opis zależności pomiędzy wartościamiargumentów, a wartościami wyników operacji.
AISD str. B
2
Przykład 1.1. Specyfikacja nieformalna automatycznego parkingu samochodowego: Parkingmoże przyjąć lub wydać samochód identyfikowany numerem rejestracyjnycm. Parking niemoże przyjąć kolejno dwóch samochodów o tym samym numerze. Jeżeli parking przyjąłsamochód to zawsze można go odebrać przez wywołanie operacji zwracania. Jeżelisamochodu nie ma na parkingu, to na żądanie zwrotu samochodu odpowiedzią jest sygnałbłędu.
Przykład 1.2. Specyfikacja typu stos z aksjomatami dla top i pop.Nazwy rodzajów nośnika: stack,elemNazwy operacji i ich arność: empty:->stack, push:stack x elem -> stack, top:stack -> elem, pop:stack -> stackRównania opisujące sposób działania operacji:∀ s:stack,e:elem top(push(s,e))=e∀ s:stack,e:elem pop(push(s,e))=s
Przykład 1.3. Przykład klasy - licznik działający cyklicznie.class Count {
nat s;public:Count() { s = 0; }void inc() { s = s + 1; if (s == 32) s = 0; }bool is_zero() { return s == 0; }
}
STRUKTURY DANYCH A POJĘCIE OBIEKTOWOŚCI
W praktyce informatyki pojęcie obiektu jest utożsamiane z fragmentem pamięci operacyjnejgdzie przechowywana jest w formie binarnej wartość jakiegoś typu, na której możnawykonywać operacje zdefiniowane przez typ danych. Jednak wygodniej jest myśleć oobiekcie jako o wartości (elemencie zbioru wartości określonego przez system typów danych).
Def. Struktura danych (zbiór obiektów z funkcjami wyznaczania następnika)Strukturę danych stanowi skończony zbiór węzłów-obiektów oraz opis bezpośredniegonastępstwa węzłów w strukturze. Bezpośrednie następstwo opisuje się z użyciem funkcji, któreodwzorowują stan obiektu zawierającego strukturę i węzeł struktury na jego następnik.Poszczególne powiązania par węzłów nazywa się też krawędziami.
W podejściu obiektowym strukturę danych wraz z algorytmami zamyka się w jedną całośćtworząc typ danych. Konkretne struktury są wtedy obiektami utworzonego typu.
W praktyce informatyki pojęcie zmiennej jest utożsamiane z fragmentem pamięci operacyjnejgdzie przechowywana jest w formie binarnej wartość jakiegoś typu, na której możnawykonywać operacje zdefiniowane przez ten typ danych. O zmiennej obiektu można teżmyśleć jako o funkcji odwzorowującej wartość obiektu na wartość składowej reprezentowanejnazwą tej zmiennej.
AISD str. C
3
Główne cechy obiektowego podejścia do implementacji typów danych to:1. Struktura danych nie występuje samodzielnie, ale wraz z zestawem operacji-funkcji
udostępniających wartości ze struktury, lub modyfikujących tą strukturę.2. Typ danych jest postrzegany jako spójna definicja, według której można tworzyć konkretne
obiekty.3. Bezpośredni dostęp do elementów struktury danych (np. przez adres) nie jest możliwy. W
konsekwencji tego wewnętrzna reprezentacja struktury danych jest ukryta w obiekcie, arodzaj struktury może być zmieniony bez zmiany działania programu jako całości.
4. Wartości struktury, pomocnicze dane, czy pomocnicze struktury są ukryte i niedostępne zzewnątrz obiektu.
5. W metodologii projektowania obiektowego program jest traktowany jako grupa obiektówwzajemnie oddziałujących na siebie (w odróżnieniu od podejścia strukturalnego, w którymprogram jest zbiorem wzajemnie powiązanych funkcji).
Metodologia programowania (projektowania) obiektowego w naturalny sposób wspiera (iwykorzystuje) koncepcję ADT. Język C++ jest zdecydowanie bogatszy, niż byłoby to niezbędnedo programowania obiektowego. Daje to z jednej strony korzyści polegające na tym, żeprogramista może kodować ten sam algorytm dla optymalizowania szybkości na wielesposobów stosując różne mechanizmy "niskiego poziomu" jak bezpośredni dostęp w dowolnemiejsce pamięci operacyjnej, czy operacje bitowe na słowach maszynowych przechowującychinformacje tekstowe i liczbowe. Z drugiej jednak strony swobodne - niezorganizowanewykorzystanie takiego dostępu i przydziału/zwalniania pamięci prowadzi do powstaniazagmatwanych nieczytelnych programów obarczonych zwykle znaczną liczbą błędów, które niedają się usunąć w tym sensie, że ich zlokalizowanie i skorygowanie kosztowałoby więcej niżzaprojektowanie i zakodowanie programu od nowa w poprawnym stylu.
Przykład 1.4: Połączenie danych i operacji w klasę zgodnie z metodyką programowaniaobiektowego.
//* (C) 1994, Saumyendra Sengupta and Carl P. Korobkin *//* and Springer-Verlag Publishing Company *// Object-oriented implementation of interval// numbers, [a, b], where a <= b.#include <stdio.h>class Interval { private: float lft_end_pt, // Left end point "a" rgt_end_pt; // Right end point "a" public: Interval(float new_lft, float new_rgt); // Constructor Interval(float new_lft_rgt); // Constr. for degenerate friend Interval operator+(Interval A, Interval B); void print_interval(char *hdr);};Interval::Interval(float new_lft, float new_rgt){ lft_end_pt = new_lft; rgt_end_pt = new_rgt;}Interval::Interval(float new_lft_rgt){
Ważnym kryterium oceny własności algorytmów jest złożoność obliczeniowa. Dla ocenyalgorytmu można wyznaczyć funkcję kosztu wykonania algorytmu. W zależności od sytuacji zajednostkowy krok obliczeniowy do wyznaczania kosztu przyjmuje się: operację arytmetyczną(algorytmy numeryczne), osiągnięcie kolejnego elementu struktury danych (algorytmy nastrukturach danych), jedno przesunięcie głowicy dysku (bazy danych). Rozróżnia się kosztmaksymalny, średni ewentualnie minimalny. Notacja O(n) [rzędu n] jest stosowana jako miaraprzy określaniu złożoności obliczeniowej.
Dla dwu matematycznych funkcji u(n) i v(n), gdzie n jest liczbą całkowitą, u(n) jest rzęduO(v(n)), jeżli dla pewnych dodatnich stałych p i q
u(n) <= p*v(n) dla wszystkich n >= q(dla wszystkich odpowiednio dużych n)
Złożoności wielomianowe:O(n) - liniowa np. u(n) = 230nO(n2) - kwadratowa np. u(n) = 12n2 + 135n - 23O(n3) - sześciena np. u(n) = n3 + 20n2 - 19n + 1
Złożoności ograniczone przez wielomian:O(log n) - logarytmiczna np. 3log (n+1) - 2O(nlog n) - quasiliniowa np. u(n) = 3nlog (n+1) - 2
Złożoności niewielomianowe:NP-zupełnaO(an) - wykładnicza np. u(n) = en + n13 - n2
AISD str. E
5
silnie NP-zupełnaNP-trudna
STRUKTURY DANYCH, A TYPY DANYCH I PROJEKTOWANIE
Abstrakcyjne typy danych określają operacje działające na obiekcie w sposób niezależny odkonkretnej implementacji. Natomiast struktury danych stanowią szczegółowe rozwiązanieimplementacyjne sposobu przechowywania danych. W rezultacie przy przy projektowaniuoprogramowania najpierw pojawiają się typy danych - ich nazwy, operacje, nazwy argumentów isposób odwzorowania argumentów na wyniki. W końcowej fazie projektowania wprowadzanesą struktury danych do celów implementacji kodu operacji. Oczywiście projekt może byćprowadzony z zamiarem wykorzystania określonego rodzaju struktury, ale nie powinno to wistotny sposób wpływać na początkowe fazy projektowania, w szczególności, w początkowychfazach projektowania nie powinny się pojawiać żadne informacje na temat używanychzmiennych wskaźnikowych, czy powiązań w strukturze danych. Te informacje szczegółowewprowadza się w ostatnim kroku projektowania (kodowanie) dla każdego z typów osobno.
Wobec powyższego niezręcznie jest mówić o typie danych "Drzewo", dlatego bo użyciedrzewiastej struktury danych jest jedną z możliwości implementacji jakiegoś projektowanegotypu danych. Podobnie niezbyt zręcznie jest mówić o abstrakcyjnym typie danych tablica. Lepiejbyłoby powiedzieć w tej sytuacji kolekcja jednoindeksowa, dwuindeksowa czy wieloindeksowa.
DEFINICJE ZWIĄZANE ZE STRUKTURAMI DANYCH
Przy omawianiu rodzajów struktur danych wygodnie będzie odwoływać się do pewnychwłasności tych struktur.
Def. SpójnośćStruktura danych jest spójna jeśli dla każdych dwóch różnych jej obiektów A, B istnieje ciągobiektów rozpoczynający się w A i kończący w B, a dla każdych dwóch kolejnych obiektów wciągu pierwszy z nich jest następnikiem drugiego lub drugi jest następnikiem pierwszego.
Def. PoprzednikPoprzednikiem obiektu X w strukturze danych jest każdy obiekt Y taki, że X jest następnikiemdla Y.
Def. PoczątkowyObiektem początkowym struktury jest każdy taki obiekt X, że dla każdego innego obiektu Y wstrukturze istnieje ciąg obiektów rozpoczynający się w X i kończący w Y, a dla każdych dwóchkolejnych obiektów w ciągu drugi z nich jest następnikiem pierwszego.
Def. Obiekt beznastępnikowyObiektem beznastępnikowym struktury jest każdy obiekt, który nie ma innych następników niżwartość null.
AISD str. F
6
Dwukierunkowym rozszerzeniem struktury danych jest struktura powstała przez dodanie dlakażdego powiązania w strukturze dodatkowego powiązania prowadzącego w przeciwnąstronę.Nie dla każdej struktury takie rozszerzenie da się skonstruować.
W ramach wykładu wyróżnione są trzy podstawowe rodzaje struktur danych tj.liniowe, drzewiaste oraz grafowe.
Def. Liniowa struktura danychStruktura danych jest liniowa gdy ma jedną funkcją określającą następnika tak, że wstrukturze występuje dokładnie jeden obiekt początkowy i dokładnie jeden beznastępnikowy,bądź też wszystkie obiekty są początkowe.
Def. Drzewiasta struktura danychStruktura danych jest drzewiasta gdy posiada dokładnie jeden obiekt początkowy, a dlakażdego obiektu poza początkowym istnieje w strukturze dokładnie jeden poprzednik.
Def. Grafowa struktura danychGrafową strukturą danych jest każda struktura danych.
WZORCOWY ZESTAW OPERACJ-METOD NA OBIEKCIE ZE STRUKTURĄ DANYCH
Przy definiowaniu typów danych należy wybrać odpowiedni zestaw operacji, który zapewniwłaściwą prostotę, elastyczność i użyteczność projektowanego typu abstrakcyjnego. Poniżejpodany jest obszerny zestaw operacji, które mogą być użyteczne przy definiowaniu typu danych,który w zamierzeniu będzie implementowany z użyciem struktur danych.
Pary operacji modyfikujące obiekt zawierający strukturę danych:Twórz obiekt z pustą strukturą danych, np. twórz-pustą-kolejkę, twórz-puste-drzewoZniszcz obiekt.Buduj strukturę z podanego ciągu wartości argumentów.Opróżnij strukturę danych w obiekcie.Dodaj element.Usuń element.Dodaj element po zadanym jej elemencie według przyjętego kryterium porządkukluczy.Usuń ze struktury element wskazany wartością klucza.Dodaj element przed/po zadanym elementem według przyjętego kryterium porządkukluczy.Usuń element przed/po zadanym elementem według przyjętego kryterium porządkukluczy.*Dodaj/usuń element przed/po zadanym jej elemencie według porządku danych wstrukturze.
AISD str. G
7
*Dodaj/usuń element wskazany dowolnym określonym położeniem w strukturze.Odczytaj/zapisz określoną wartość składową wskazanego elementu strukturydanych.Sortuj strukturę według określonego porządku.Przywróć stan sprzed ostatniej modyfikacji.Cofnij się o kolejny krok wstecz (z postacią struktury) w wykonanychmodyfikacjach.
Operacje testowania zawartości struktury:Sprawdź, czy struktura jest pusta.Podaj liczbę/rozmiar elementów struktury.Sprawdź, czy w strukturze występuje element o danej wartości.*Szukaj nstępnika (poprzednika) elementu w strukturze.*Zbadaj pewną własność struktury (np. spójność, istnienie końcowego)*Zbadaj pewną własność elementu struktury (np. beznastępnikowość).
Operacje konwersji:Dokonaj konwersji struktury danych na postać tekstową.Przywróć strukturę z postaci tekstowej.Spakuj strukturę do postaci binarnej.Przywróć strukturę z postaci binarnej.
Operacje na całych strukturach:Dosumuj informację z jednej struktury danych do drugiej.Połącz/dodaj informację z dwóch struktur.Odejmij informację z jednej struktury od drugiej struktury.Znajdź przecięcie informacji ze struktur tworząc nową strukturę.
Operacje obsługi wyjątków:Obsługa braku pamięci.Obsługa próby odczytania czegoś według adresu = NULLObsługa błędów przy obliczeniach, kopiowaniu wartości przechowywanych wstrukturze.
Operacje diagnostyczno-kontrolne:Sprawdź poprawność postaci struktury.Koryguj określony błąd struktury.Dokonaj autokorekcji.
Operacje optymalizujące efektywność i zajętość pamięci:Sprawdź stan wyważenia/wypełnienia struktury danych.Wyważaj/zreorganizuj połaczenia w strukturze danych.Buduj indeks przeszukiwań według wskazanego kryterium.
Operacje reorganizujące atrybuty/typy informacji przechowywane w strukturze danych:Dodaj atrybut/pole informacyjne do określonych obiektów struktury.Skasuj atryput/pole w określonych obiektach struktury.Odczytaj rodzaje atrybutów.
Operacje bez gwiazdek można zawsze zdefiniować (w definicji typu danych) abstrachując odpostaci struktury. Operacje te odpowiadałyby charakterystycznym operacjom dla typu danych.
AISD str. H
8
Operacje z gwiazdkami uzależniają zwykle w dużym stopniu sposób operowania na obiekcie zestrukturą danych od wybranej postaci struktury i dlatego należy ich zdecydowanie unikać.
Dużym błędem metodycznym jest stworzenie możliwości dostępu do struktury danych bezpośrednictwa operacji zdefiniowanych dla obiektu. Dzieje się tak wtedy, gdy na zewnątrzobiektu dostępny jest adres elementu struktury, co jest możliwe gdy:1. Operacja dodawania obiektu do struktury nie kopiuje go, ale bezpośrednio włącza do
struktury.2. Operacja przeglądania struktury lub operacja odczytu wartości obiektu zwraca adres obiektu
ze struktury danych, a nie kopię informacji o obiekcie.3. W obiektach występują zmienne ze słowem kluczowym static.
LINIOWE STRUKTURY DANYCH
Def. Liniowa struktura danychStruktura danych jest liniowa gdy ma jedną funkcją określającą następnika tak, że wstrukturze występuje dokładnie jeden obiekt-węzeł początkowy i dokładnie jedenbeznastępnikowy, bądź też wszystkie obiekty są początkowe.
Przykładami struktur liniowych i ich rozszerzeń dwukierunkowych są lista, listadwukierunkowa, pierścień, pierścień dwukierunkowy. Z użyciem struktur liniowych możnazaimplementować dowolny typ danych, ale zwykle struktry liniowe przypisuje się typomsekwencja, kolejka i stos.
Jako pierwszy przykład będzie przedstawiony typ sequence, a następnie pokazane będą jegodwie implementacje. Typ sekwencja zawiera zaledwie kilka nieskomplikowanych operacji:
Twórz obiekt - pustą sekwencję.Niszcz obiekt.Sprawdź, czy sekwencja jest pusta.Sprawdź, czy występuje element o wartości/atrybucie.Usuń element.Dodaj element po określonym elemencie.Podaj liczbę elementów w sekwencji.Dokonaj konwersji na postać tekstową.
Implementacja sekwencji może być zrealizowana z wykorzystaniem osobno przydzielonychbloków pamięci operacyjnej połączonych wskaźnikami, lub z wykorzystaniem ciągłego obszarupamięci. Tutaj wykorzystany będzie przydział osobnych bloków pamięci. Zasady wykorzystaniaciągłego obszaru pamięci będą omówione w rozdziale o tablicach i reprezentacji struktur wpamięci.
Obiektowa implementacja sekwencji z użyciem listy jednokierunkowej z wykorzystaniemwskaźników:#include <stdio.h>typedef int BOOLEAN;typedef char DATA_TYPE;#include "bc_sequence.h" // For base classclass Singly_Linked_List : public Sequence { private: typedef struct SLIST_ELEMENT { DATA_TYPE data; SLIST_ELEMENT *next; } *LIST_PTR; LIST_PTR head_ptr; // Ptr to first element in list void init_slist() { head_ptr = NULL;}; protected: LIST_PTR search_element1(LIST_PTR, DATA_TYPE); LIST_PTR search_previous(LIST_PTR, DATA_TYPE); LIST_PTR get_head(void) { return head_ptr; } BOOLEAN is_sublist_empty(LIST_PTR lst_ptr); public: Singly_Linked_List() { init_slist(); } // Constructor ~Singly_Linked_List(); // Destructor BOOLEAN is_empty() {return (head_ptr == NULL);} void build_list (DATA_TYPE *A); void search_element (DATA_TYPE srch_key); void add_after (DATA_TYPE elt_after, DATA_TYPE new_elt); void delete_element (DATA_TYPE target_key); int get_elt_numbers(void); void print_list(char *hdr);};
Singly_Linked_List::~Singly_Linked_List(void){ LIST_PTR next_ptr, tmp_ptr; tmp_ptr = head_ptr; while (!is_sublist_empty(tmp_ptr)) { next_ptr = tmp_ptr->next; delete tmp_ptr; // Dispose of its space tmp_ptr = next_ptr; } head_ptr = NULL; // avoid a "dangling pointer"} BOOLEAN Singly_Linked_List::is_sublist_empty( LIST_PTR lst_ptr){ return (lst_ptr == NULL);}void Singly_Linked_List::build_list(DATA_TYPE *str){ LIST_PTR tmp_ptr, new_ptr; while (*str != '\0') { new_ptr = new SLIST_ELEMENT; new_ptr->data = *str++ ; new_ptr->next = NULL;
AISD str. J
10
if (head_ptr == NULL) { head_ptr = new_ptr; tmp_ptr = new_ptr; } else { tmp_ptr->next = new_ptr; tmp_ptr = tmp_ptr->next; } }}Singly_Linked_List::LIST_PTRSingly_Linked_List::search_element1(LIST_PTR lst_ptr,DATA_TYPE search_key){ if (!is_sublist_empty(lst_ptr)) { if (search_key == lst_ptr->data) return (lst_ptr); search_element1 (lst_ptr->next, search_key); } else { printf("\n search_element: %s \n", "Element is not found in list"); return (NULL); }}
void Singly_Linked_List::search_element(DATA_TYPE search_key){ if (search_element1 (head_ptr, search_key) != NULL) printf("\n Element is found %s \n", "in singly linked list");}
Singly_Linked_List::LIST_PTRSingly_Linked_List::search_previous (LIST_PTR lst_ptr, DATA_TYPE search_key){ if (lst_ptr != NULL) { if (search_key == lst_ptr->next->data) return (lst_ptr); search_previous (lst_ptr->next, search_key); } else { printf("\n search_previous: Previous %s \n", "element is not found in list"); return (NULL); }}
void Doubly_Linked_List::search_element(DATA_TYPE search_key){ if (search_elt_obj(head_ptr, search_key) != NULL) printf("\n Element is found %s \n", "in doubly linked list object.");}void Doubly_Linked_List::delete_element(DATA_TYPE element_key){ LIST_PTR lst_ptr = head_ptr, search_ptr, tmp_ptr; search_ptr = search_elt_obj(lst_ptr, element_key); if (search_ptr == NULL) { // object is not found printf("\n delete_element: Object to be %s \n", "deleted is not found"); return; } if (search_ptr == head_ptr) { tmp_ptr = head_ptr->next; if (tail_ptr == head_ptr) tail_ptr = tmp_ptr; delete head_ptr; // Free up memory head_ptr = tmp_ptr; head_ptr->prev = NULL; return; } if (search_ptr == tail_ptr) { tail_ptr = search_ptr->prev; tail_ptr->next = NULL; delete search_ptr; // Free up memory return; } search_ptr->prev->next = search_ptr->next; search_ptr->next->prev = search_ptr->prev; delete search_ptr;}int Doubly_Linked_List::get_elt_numbers(void){ LIST_PTR lst_ptr = head_ptr; int obj_numbers = 0; while (lst_ptr != NULL) { ++obj_numbers; lst_ptr = lst_ptr->next; } return(obj_numbers);}void Doubly_Linked_List::print_dlist_obj_forward(void){ LIST_PTR lst_ptr = head_ptr; while (lst_ptr != NULL) { printf("%c <-> ", lst_ptr->data); lst_ptr = lst_ptr->next; } printf("NULL \n"); // NULL indicates last elt of list}void Doubly_Linked_List::print_dlist_obj_backward(void){ LIST_PTR lst_ptr = tail_ptr;
while (lst_ptr != NULL) { printf("%c <-> ", lst_ptr->data);
AISD str. N
14
lst_ptr = lst_ptr->prev; } printf("NULL \n"); // NULL indicates last elt of list}
void main(void){ Doubly_Linked_List dlist_obj; char *str = "LOPAMUDRA"; // Input string printf("\n ** OOP IMPLEMENTATION OF %s \n", "DOUBLY LINKED LIST ** " ); dlist_obj.build_list (str); dlist_obj.print_list("(forward)"); printf("\n The list object (backward) is:\n "); dlist_obj.print_dlist_obj_backward(); printf(" %s in this list object is: %d \n", "Number of elements", dlist_obj.get_elt_numbers()); dlist_obj.delete_element ('R'); dlist_obj.print_list("after deleting \'R\'"); printf(" %s in this list object is: %d \n", "Number of elements", dlist_obj.get_elt_numbers()); delete str;}
W pierścieniach bezpośrednim następnikiem dla elementu ostatniego jest element pierwszy.1. Należy sprawdzać warunek powrotu do elementu startowego (ang. wraparound condition).
Rozpoczynając przegladanie od dowolnego elementu listy, np. E, musi się ono na jegopoprzedniku zakończyć.
2. Sprawdzanie warunku powrotu do elementu startowego zapobiega wystąpieniunieskończonego cyklicznego przeglądania.
3. Gdy wskazanie na poczatkowy element jest puste (NULL) - pierścień jest pusty.KOLEJKA
Typ kolejka służy do przechowywania informacji-obiektów w postaci sekwencji. Elementywprowadzane do kolejki umieszcza się na końcu, a kolejny element wyjmuje się z początkukolejki. Przykładowe metody kolejki:
Utwórz i inicjalizuj obiekt kolejkiZniszcz kolejkęSprawdź, czy kolejka jest pustaSprawdź, czy kolejka jest pełna (dla tablicy)Buduj kolejkę z podanego zbioru elementówDodaj element do kolejki (enqueue)Usuń element z kolejki (dequeue)Pokaż (zamień na ciąg znaków) kolejkę
void Singly_Linked_Queue::print_que (void){ if (!is_que_empty()) { for (QUEUE_PTR tmp_ptr = front_of_queue; tmp_ptr != NULL; tmp_ptr = tmp_ptr->next) printf(" %c -> ", tmp_ptr->data); printf("NULL\n \^"); printf("\n !___ Front of this queue object \n"); } else printf ("\n print_que: Empty Queue.\n");}int Singly_Linked_Queue::get_que_siz(void){ printf("\n get_que_siz: Exercise !!\n"); return(0); // To avoid compilation warning}void main (void){ Singly_Linked_Queue lnk_que_obj; static char str[] = "SAUREN"; printf("\n ** OOP IMPLEMENTATION OF %s ** \n", "SINGLY LINKED QUEUE"); printf("\n Queue representation of \"%s\" is:\n", str); lnk_que_obj.build_que (str); lnk_que_obj.print_que(); printf("\n After two remove %s \n", "operations, queue is:"); lnk_que_obj.del_from_que(); lnk_que_obj.del_from_que(); lnk_que_obj.print_que();}
Stos
Typ stos definiuje obiekty, do których można wkładać i wyjmować obiekty ustalonego typuwedług kolejności LIFO czyli ostatni przyszedł - pierwszy wyszedł. Przykładowe metody stosu:
Utwórz i inicjalizuj obiekt stosuZniszcz stosSprawdź, czy stos jest pustySprawdź, czy stos jest pełny (dla tablicy)Buduj stos z podanego zbioru elementówDodaj element na stos (push)Usu_ element ze stosu (pop)Pobierz atrybut elementu z wierzchołka stosuModyfikuj atrybut elementu na wierzchołku stosuPokaż (drukuj) stosPodaj liczbę elementów na stosie
void Lnk_Stack::build_stack(DATA_TYPE str[]){ if (str[0] == '\0') // End of string printf("\n build_stack: Empty string.\n"); else for (int j = 0; str[j] != '\0'; ++j) push(str[j]);}DATA_TYPE Lnk_Stack::get_top_of_stack(void){ if (!is_empty()) return(top_of_stack->data); else printf("\n get_top_of_stack: %s \n", "No Element in Stack.");}void Lnk_Stack::print_stack(void){ if (!is_empty()) { for (STACK_PTR tmp_ptr = top_of_stack; tmp_ptr != NULL; tmp_ptr = tmp_ptr->next) printf(" %c -> ", tmp_ptr->data); printf("NULL\n \^"); printf("\n !___ Top of this stack object \n"); } else printf("\n No Element in Stack.\n");}void main(void){ Lnk_Stack lnk_stk_obj; static char str[] = "SAUREN"; printf("\n ** OOP IMPLEMENTATION OF %s ** \n", "SINGLY LINKED STACK"); printf("\n Stack representation of \"%s\" is: \n ", str); lnk_stk_obj.build_stack(str); lnk_stk_obj.print_stack(); delete []str;}
Kolejka priorytetowa
Typ kolejka priorytetowa służy do przechowywania informacji-obiektów w postaci sekwencjiuporządkowanej według priorytetu. Elementy wprowadzane do kolejki umieszcza się wmiejscuodpowiadającemu priorytetowi, a kolejny element wyjmuje się z początku kolejki. Przykładowemetody kolejki:
Utwórz i inicjalizuj obiekt kolejkiZniszcz kolejk_Sprawdź, czy kolejka jest pustaSprawdź, czy kolejka jest pełna (dla tablicy)Buduj kolejkę z podanego zbioru elementówDodaj element do kolejki (enqueue)Usuń element z kolejki (dequeue)Pokaż (drukuj) kolejkę
Implementacja kolejki priorytetowej jest analogiczna jak zwykłej kolejki z wyjątkiem operacjiwstawiania, gdzie wstawia się element w środek listy według priorytetu.
Podsumowanie do liniowych struktur danych:1. Typ sekwencja można zaimplementować z użyciem następujących struktur liniowych:
- tablice- listy jednokierunkowe- pierścienie jednokierunkowe- ich dwukierunkowe rozszerzenia
2. Złożoność obliczeniowa dla operacji wymagających przeszukania struktury liniowej wynosiO(n).
3. Dla sekwencji implementowanych przy pomocy tablic operacje wstawiania i usuwaniaelementów środku są nieefektywne (wymagają przemieszczania elementów). Pamięć możebyć niewykorzystana, gdy występuje zbyt mała liczba elementów.
4. Lista dynamicznie zmienia zajętość pamięci.5. Przeglądanie list jedno- i dwukierunkowej rozpoczyna się od głowy i jest wykonywane
sekwencyjnie.6. Do przeglądania pierścieni wystarczy wskazanie na dowolny element.7. Wybór metody implementacji sekwencji zależy od możliwości i kosztów dynamicznego
przydziału pamięci.8. Liniowe struktury danych są najprostszymi do implementacji stosów, kolejek, a ponadto
można je w zasadzie stosować do makietowych implementacji dowolnego typu danych.9. Najczęstszymi błędami w praktyce implementacji w jęz. C++ struktur danych są: wskazania
do zwolnionej pamięci i utrata dostępu do przydzielonej pamięci.
AISD str. W
23
SORTOWANIE I WYSZUKIWANIE
Metody sortowania:- według klucza (wybranej wartości)- według cyfrowych własności klucza (bitów) (ang. radix sorts)
Kategorie sortowania:- wewnętrzne sortowanie (dane są w pamięci operacyjnej)- zewnętrzne sortowanie (dane w zbiorach danych)
Proste metody sortowania:- przez wstawianie (ang. Insertion Sort)- wstawianie do listy (ang. Linked List Insertion Sort)- przez wybieranie (ang. Selection Sort)- sortowanie bąbelkowe (ang. Bubble Sort)
Zaawansowane metody sortowania:- Shell Sort- Quick Sort- Merge Sort- Binary Tree Sort- Heap Sort- Straight Radix Sort- Radix Exchange Sort
class Base_Sort {
public:
void build_list (DATA_TYPE input[]);
void debug_print (int iteration, int debug, char *hdr);
void Insertion_Sort (DATA_TYPE input[]);
void Selection_Sort (DATA_TYPE input[]);
void Bubble_Sort (DATA_TYPE input[]);
void Quick_Sort (DATA_TYPE input[]);
void Heap_Sort (DATA_TYPE A[]);
void Radix_Exch_Sort (DATA_TYPE input[], int bitnum);
void Insert_Sort_dlist (void); // Doubly linked list sort
};
Sortowanie przez wstawianie (ang. Insertion Sort)
#include <stdio.h>
typedef int DATA_TYPE;
typedef DATA_TYPE ARY_LIST[];
class Sort {
private:
AISD str. X
24
DATA_TYPE *A; // Array list A[]
int n; // size of A
public:
Sort (int size) { A = new DATA_TYPE[n=size]; }
~Sort() { delete []A; }
void build_list (DATA_TYPE input[]);
void debug_print (int iteration, int debug, char *hdr);
void Insertion_Sort (DATA_TYPE input[]);
};
void Sort::build_list (ARY_LIST input)
{
for (int i = 0; i < n; i++)
A[i] = input[i];
}
void Sort::debug_print (int iteration, int debug, char *hdr)
{
if (debug == 0)
printf("\n %s \n", hdr);
else
printf(" Pass #%d:", iteration + 1);
for (int i = 0; i < n; i++)
printf(" %d", A[i]);
printf("\n");
}
void Sort::Insertion_Sort (ARY_LIST input)
{
build_list(input); // Build array A
debug_print (0, 0, "List to be sorted in ascending order:");
int begin = n - 2;
for (int k = begin; k >= 0; k--) { // Outer loop
int i = k + 1;
DATA_TYPE swap_area = A[k];
while (swap_area > A[i]) { // inner loop
A[i - 1] = A[i];
i++;
}
A[i - 1] = swap_area;
debug_print (n - 2 - k, 1, "");
}
}
void main(void)
{
Sort insert_srt_obj (10);
static ARY_LIST A = {33, 60, 5, 15, 25,
12, 45, 70, 35, 7 };
printf("\n ** OOP IMPLEMENTATION OF %s",
"INSERTION SORT **");
insert_srt_obj.Insertion_Sort (A);
insert_srt_obj.debug_print (0, 0,
"List sorted using insertion sort:");
}
AISD str. Y
25
Sortowanie przez wstawianie ma złożoność O(n2) dla najgorszego przypadku. Jest proste iwydajne dla małego n. Wadą jest to, że umieszcza prawidłowo tylko jeden element dlapojedynczego kroku.
AISD str. Z
26
Sortowanie przez wstawianie do listy (ang. Linked List Insertion Sort)
#include <stdio.h>
#include <stdlib.h>
const int FOREVER = 1;
typedef int DATA_TYPE;
class Doubly_Linked_List {
private:
typedef struct DLIST_ELEMENT {
DATA_TYPE data;
DLIST_ELEMENT *next;
DLIST_ELEMENT *prev;
} *DLLIST_PTR;
DLLIST_PTR head_ptr, tail_ptr, curr_ptr;
void init_dlist (void);
public:
Doubly_Linked_List () { init_dlist(); }
~Doubly_Linked_List () { };
void build_dlist (void);
void Insert_Sort_dlist (void);
void print_dlist (void);
};
void Doubly_Linked_List::init_dlist (void)
{
head_ptr = tail_ptr = curr_ptr = NULL;
}
void Doubly_Linked_List::build_dlist (void)
{
DLLIST_PTR prev_ptr = NULL,
new_ptr;
char buffer[81];
printf ("\n ** OOP FOR INSERTION SORT OF %s \n\n",
"A DOUBLY LINKED LIST **");
while (FOREVER) {
printf ("\nCreate List - <Return> with no Value to Quit");
printf ("\nEnter an Integer: ");
gets (buffer);
if (buffer[0] == '\0') {
tail_ptr = curr_ptr; // initialize tail pointer
return;
}
new_ptr = new DLIST_ELEMENT;
new_ptr->data = atoi (buffer);
new_ptr->next = NULL;
new_ptr->prev = NULL;
curr_ptr = new_ptr;
if (head_ptr == NULL) // initialize head pointer
head_ptr = curr_ptr;
else {
curr_ptr->prev = prev_ptr; // link element
prev_ptr->next = curr_ptr;
AISD str. AA
27
}
prev_ptr = curr_ptr;
}
}
AISD str. BB
28
void Doubly_Linked_List::Insert_Sort_dlist (void)
{
DLLIST_PTR curr_ptr = head_ptr, search_ptr;
while ((curr_ptr = curr_ptr->next) != NULL) {
search_ptr = curr_ptr->prev;
while (search_ptr != NULL && curr_ptr->data < search_ptr->data)
search_ptr = search_ptr->prev;
if ((curr_ptr->prev != search_ptr) && (search_ptr != NULL ||
curr_ptr->data < head_ptr->data)) {
curr_ptr->prev->next = curr_ptr->next;
if (curr_ptr == tail_ptr)
tail_ptr = curr_ptr->prev;
else
curr_ptr->next->prev = curr_ptr->prev;
if (search_ptr != NULL) {
curr_ptr->prev = search_ptr;
curr_ptr->next = search_ptr->next;
search_ptr->next->prev = curr_ptr;
search_ptr->next = curr_ptr;
}
else { // curr_ptr->data < head_ptr->data
curr_ptr->prev = NULL;
curr_ptr->next = head_ptr;
head_ptr->prev = curr_ptr;
head_ptr = curr_ptr;
}
}
}
}
void Doubly_Linked_List::print_dlist (void)
{
DLLIST_PTR tmp_ptr = head_ptr;
printf ("NULL <-> ");
while (tmp_ptr != NULL) {
printf ("%d <-> ", tmp_ptr->data);
tmp_ptr = tmp_ptr->next;
}
printf ("NULL \n");
}
void main (void)
{
Doubly_Linked_List dlist_obj;
dlist_obj.build_dlist ();
printf ("\nDoubly Linked List Before %s \n", "Insertion Sort is:");
dlist_obj.print_dlist ();
printf ("\nDoubly Linked List After %s \n", "Insertion Sort is:");
dlist_obj.Insert_Sort_dlist ();
dlist_obj.print_dlist ();
}
AISD str. CC
29
Sortowanie przez wybieranie (ang. Selection Sort)
#include <stdio.h>
class Sort {
private:
typedef int DATA_TYPE;
typedef DATA_TYPE ARY_LIST[];
DATA_TYPE *A; // Array list A[]
int n; // size of A
public:
Sort (int size) { A = new DATA_TYPE[n=size]; }
~Sort() { delete []A; }
void build_list (DATA_TYPE input[]);
void debug_print (int iteration, int debug, char *hdr);
void Selection_Sort (DATA_TYPE input[]);
};
void Sort::build_list (ARY_LIST input)
{
for (int i = 0; i < n; i++)
A[i] = input[i];
}
void Sort::debug_print (int iteration, int debug, char *hdr)
{
if (debug == 0)
printf("\n %s \n", hdr);
else
printf(" Pass #%d:", iteration + 1);
for (int i = 0; i < n; i++)
printf(" %d", A[i]); printf("\n");
}
void Sort::Selection_Sort (ARY_LIST input)
{
build_list(input); // Build array A
debug_print (0, 0, "List to be sorted in ascending order:");
printf("\n ** OOP IMPLEMENTATION OF %s", "BUBBLE SORT **");
bubble_srt_obj.Bubble_Sort (A);
bubble_srt_obj.debug_print (0, 0, "List sorted using bubble sort:");
}
Złożoność obliczeniowa dla najgorszego przypadku jest O(n2).
Wady metody:1. W pojedynczym kroku iteracji tylko jeden element osiąga swoją pozycję.2. Liczba operacji wymiany elementów (wzajemnej) może osiągać w kazdym kroku
iteracji wewnętrznej liczbę (n - iteration - i). Duże wymagania na przemieszczanieelementów czynią sortowanie bąbelkowe nieefektywnym dla złożonych strukturdanych.
3. Metoda ta silnie zależy od porównywania i zamiany tylko kolejnych elementów.
Quicksort
Quicksort oparty jest o 3 główne strategie:- podział na podtablice- sortowanie podtablic- połączenie posortowanych podtablic
Algorytm rekursywny Quicksort.
Algorytm dzielenia tablicy:1. Element wybrany Pivot = A[First]2. Inicjalizacja dwóch indeksów
i = First (dolny indeks (pod)tablicyj = Last (górny indeks (pod)tablicy
3. Dopóki A[i] <= Pivot and i < Last zwiększaj i o 1. W przeciwnym przypadkuzaprzestaj zwiększania.
4. Dopóki A[j] >= Pivot and j > First zmniejszaj j o 1. W przeciwnym przypadkuzaprzestaj zmniejszania.
5. Jeśli i < j zamień wartości A[i] i A[j].6. Powtarzaj kroki 2. - 4. aż i > j (niespełnienie 5.)7. Zamień miejscami Pivot i A[j].
AISD str. FF
32
Przykład pierwszego kroku dlaA[] = {33, 60, 5, 15, 25, 12, 45, 70, 35, 7}
#include <stdio.h>
class Sort {
private:
typedef int DATA_TYPE;
typedef DATA_TYPE ARY_LIST[];
DATA_TYPE *A; // Array list A[]
int n; // size of A
int iter;
public:
Sort (int size) {iter =0; A = new DATA_TYPE[n=size];}
~Sort() { delete []A; }
void build_list (DATA_TYPE input[]);
void debug_print (int iteration, int debug, char *hdr);
void qsort (int First, int Last);
void Quick_Sort (DATA_TYPE input[]);
};
void Sort::build_list (ARY_LIST input)
{
for (int i = 0; i < n; i++)
A[i] = input[i];
}
void Sort::debug_print (int iteration, int debug, char *hdr)
{
if (debug == 0)
printf("\n %s \n", hdr);
else
printf(" Pass #%d:", iteration + 1);
for (int i = 0; i < n; i++)
printf(" %d", A[i]);
printf("\n");
}
void Sort::qsort (int First, int Last)
{
if (First < Last) {
DATA_TYPE Pivot = A[First];
int i = First;
int j = Last;
while (i < j) {
while (A[i] <= Pivot && i < Last)
i += 1;
while (A[j] >= Pivot && j > First)
j -= 1;
if (i < j) { // Swap the Pivots
DATA_TYPE swap_area = A[j];
A[j] = A[i];
A[i] = swap_area;
}
}
DATA_TYPE swap_area = A[j];
AISD str. GG
33
A[j] = A[First];
A[First] = swap_area;
debug_print (iter++, 1, "");
qsort (First, j - 1);
qsort (j + 1, Last);
}
}
void Sort::Quick_Sort (ARY_LIST input)
{
build_list(input); // Build array A
debug_print (0, 0, "List to be sorted in ascending order:");
printf("\n ** OOP IMPLEMENTATION OF %s", "QUICK SORT **");
quick_srt_obj.Quick_Sort (A);
quick_srt_obj.debug_print (0, 0, "List sorted using quick sort:");
}
Złożoność obliczeniowa Quicksort jest dla najlepszego przypadku i średnio O(nlogn). Dlanajgorszego przypadku złożoność obliczeniowa jest O(n2). Implementacja wymagaintensywnego wykorzystywania stosu. Operacja podziału jest złożona, zaś operacja scalaniawyników jest prosta.
Merge Sort
Merge Sort jest metodą stosowaną do danych zewnętrznych (plików). Algorytm zawiera 3części: podział, sortowanie, łączenie danych. Można wyróżnić 3 wersje Merge Sort:- iteracyjną- rekursywną- ze wskazaniem (ang. link)
Algorytm Merge Sort (wersja iteracyjna dla pliku):1. Otwórz plik wejściowy nieposortowany UF do czytania i pisania, oraz dwa pliki
robocze TMP_F1 i TMP_F2 do pisania.2. (Split) Kopiuj po 1 (20) elemencie pliku UF naprzemiennie do plików TMP_F1 i
TMP_F2.3. (Merge) Porównaj jednoelementowe grupy z TMP_F1 i z TMP_F2, zapisz je po
porównaniu do UF - najpierw mniejszą.4. (Split) Kopiuj po 2 (21) elementy pliku UF naprzemiennie do plików TMP_F1 i
TMP_F2.5. (Merge) Porównaj dwuelementowe grupy z TMP_F1 i z TMP_F2, zapisz elementy
po porównaniu do UF - najpierw mniejszy.6. Powtarzaj proces podziału (Split) i łączenia (Merge) jak dla punktów 4. - 5. dla grup
o rozmiarach 2i dla i = 2, 3, ..., log2n.7. Plik jest posortowany rosnąco. Zamknij pliki i usuń robocze.
Wady metody iteracyjnej:1. Nie jest uwzględniane, że część danych może być posortowana.2. Metoda wymaga dwóch dodatkowych plików, każdy o rozmiarze n (maksymalnie).3. Liczba elementów w plikach roboczych może być różna.
#include <stdio.h>
AISD str. II
35
#include <stdlib.h>
#define MIN(x,y) ( (x <= y) ? x : y )
typedef int DATA_TYPE;
enum STATUS {UNSORTED, SORTED, DATA_AVAILABLE, END_OF_FILE};
void Merge_Sort (char *sorted_file_name)
{
FILE *sorted_file, *tmp_file_1, *tmp_file_2;
enum STATUS status = UNSORTED, status_a, status_b;
DATA_TYPE a, b, last = 0;
int file = 1;
if ((sorted_file = fopen (sorted_file_name, "r+"))== NULL ||
Algorytm Merge Sort (wersja rekursywna dla pliku):1. Otwórz plik wejściowy nieposortowany UF do czytania i pisania, oraz dwa pliki
robocze TMP_F1 i TMP_F2 do pisania.2. (Split) Kopiuj po połowie pliku UF do plików TMP_F1 i TMP_F2.3. Wywołaj procedurę sortowania dla lewej połówki (TMP_F1).4. Wywołaj procedurę sortowania dla prawej połówki (TMP_F2).5. (Merge) Kopiuj posortowane dane z TMP_F1 i z TMP_F2 do UF.6. Plik jest posortowany rosnąco. Zamknij pliki i usuń robocze.
Algorytm Merge Sort (wersja ze wskazaniem dla tablicy):
Weźmy rekordy danych o postaci:typedef int KEY_TYPE;
typedef struct DATA {
KEY_TYPE key;
int link;
} ARY_LIST[SIZE];
Chcemy posortować tablicę tak, aby wskazanie (link) było indeksem kolejnego większegoelementu. Użyjemy do tego funkcję:
Link_Merge_Sort (A, First, Last, Index_of_lowest_element)
0. Wszystkie pola link mają wartość -1 (nieistniejący indeks).1. Jeśli First >= Last, ustaw Index_of_lowest_element = First i zakończ. W
przeciwnym przypadku wykonaj kroki 2. - 6.2. Podziel tablicę na połowy, Middle = (First + Last) / 2 .3. Wykonaj algorytm dla lewej połowy
Link_Merge_Sort (A, First, Middle, Left_Begin)4. Wykonaj algorytm dla prawej połowy
Link_Merge_Sort (A, Middle+1, Last, Right_Begin)5. Połącz dwie podtablice używając Left_Begin i Right_Begin. Przemieszczanie
wartości nie jest konieczne.6. Ustaw Index_of_lowest_element na najmniejszy element tablicy A.
Główne cechy powyższego algorytmu:1. Prostota (dzielenie na połowy).2. Nie potrzeba dodatkowych podtablic na podział.
AISD str. LL
38
3. Nie wymaga przemieszczania elementów - początkowe pozycje tablicy A pozostająniezmienione.
4. A[Index_of_lowest_element] zawiera najmniejszy element. Następna wartość jestA[A[Index_of_lowest_element].link].
Złożoność obliczeniowa Merge Sort dla najgorszego przypadku wynosi O(nlogn). Strategiapodziału jest oparta na dzieleniu na połowy (log2n) a łączenie jest liniowe (n).
AISD str. MM
39
Sortowanie z pomocą drzewa binarnego (ang. Binary Tree Sort)
Elementy tablicy (listy) wstawiamy do budowanego drzewa binarnego uporządkowanego zewzględu na wartość kluczy. Następnie przeglądając drzewo kopiujemy elementy doposortowanej tablicy.
Heap Sort (sortowanie z pomocą sterty)
Sterta jest drzewem binarnym o następujących własnościach:
1. Jest kompletna, tzn. liście drzewa są na co najwyżej dwóch poziomach i liście naostatnim (najniższym) poziomie są umieszane od lewej strony.
2. Każdy poziom w stercie jest wypełniony od strony lewej do prawej.3. Jest częściowo uporządkowana, tzn. wartości kluczy w każdym węźle są w
określonej relacji porządkującej (np. >=, <=) w stosunku do węłów podrzędnych(dzieci).
Sa trzy główne kategorie stert:1. max heap (dzieci <= rodzica)2. min heap (dzieci >= rodzica)3. min-max heap (poziomy w stercie naprzemiennie spełniają warunki 1. i 2.)
70 (a) Max heap
60 45
35 25 05 12
15 33 07
05 (b) Min heap
07 12
35 15 33 45
70 60 25
05 (c) Min-max heap
70 45
15 07 12 33
60 35 25
Tworzenie sterty (max heap) - przykład.Zasada sortowania z wykorzystaniem sterty.
Metody obiektu sterty:Twórz i inicjalizuj stertę.Buduj stertę (max heap) z elementów tablicy A.Zniszcz stertę (i tablicę A).Porównuj elementy danych.Zamień miejscami elementy danych.
AISD str. NN
40
Przebuduj stertę (aby zachować jej własności).Wykonaj sortowanie dla tablicy A z wykorzystaniem sterty.Drukuj stertę dla każdej iteracji sortowania.Drukuj stertę.
Sterta często jest przechowywana w tablicy w taki sposób, że korzeń jest umieszczony welemencie o indeksie 1, a dzieci każdego węzła o indeksie i mają indeksy 2*i oraz 2*i+1.
#include <iostream.h> // For 'cin' & 'cout'
typedef int DATA_TYPE; // Also the 'key'
class Heap {
DATA_TYPE *heap;
int heap_size;
void Init_Heap (DATA_TYPE A[]);
public:
Heap (int n); // Constructor
~Heap () { delete [] heap; } // Destructor
void ReAdjust_Heap (int Root, int Max_Elem);
void Build_Heap (void);
void Debug_Print (int pass, int reduced_heap_size);
void Heap_Sort (DATA_TYPE A[]);
void Print_Heap (void);
};
Heap::Heap (int n)
{
heap = new DATA_TYPE [n + 1];
heap_size = n;
}
void Heap::Init_Heap (DATA_TYPE A[])
{
for (int i = 1; i <= heap_size; i++)
heap[i] = A[i - 1];
}
void Heap::ReAdjust_Heap (int Root, int Max_Elem)
{
enum BOOLEAN {FALSE, TRUE};
BOOLEAN Finished = FALSE;
DATA_TYPE x = heap[Root];
int j = 2 * Root; // Obtain child information
while ((j <= Max_Elem) && (!Finished)) {
if ((j < Max_Elem) && (heap[j] < heap[j + 1]))
j++;
if (x >= heap[j])
Finished = TRUE;
else {
heap[j/2] = heap[j];
j = 2 * j;
}
} // while
heap[j/2] = x;
AISD str. OO
41
}
void Heap::Debug_Print (int pass, int reduced_heap_size)
{
cout << " Pass #" << pass << ": ";
for (int i = 1; i <= reduced_heap_size; i++)
cout << heap[i] << " ";
cout << " | ";
for (; i <= heap_size; i++)
cout << heap[i] << " ";
cout << "\n";
}
void Heap::Build_Heap (void)
{
for (int i = heap_size/2; i > 0; i--)
ReAdjust_Heap (i, heap_size);
}
void Heap::Heap_Sort (DATA_TYPE A[])
{
Init_Heap (A);
Build_Heap (); // Build a max heap
for (int i = (heap_size - 1); i > 0; i--) {
int tmp = heap[i + 1]; // swap
heap[i + 1] = heap[1];
heap[1] = tmp;
A[i] = heap[i + 1];
ReAdjust_Heap (1, i); // Rebuild max heap
#ifdef DEBUG
Debug_Print ((heap_size - i), i);
#endif
}
A[0] = heap[1]; // Put last element of heap in A
}
void Heap::Print_Heap (void)
{
cout << "\n ** SORTED IN ASCENDING ORDER USING HEAP SORT"
" **\n";
for (int i = 1; i <= heap_size; i++)
cout << " " << heap[i];
cout << "\n";
}
void main (void)
{
int n;
cout << "\nEnter the number of elements to be sorted: ";
W systemie liczbowym o podstawie (ang. radix) r jest r cyfr 0, 1, 2, ..., (r-1) i każdą liczbę odługości n (liczba cyfr) można przedstawić następująco:
k = d1d2...dm
d1 - to MSD (Most Significant Digit)dm - to LSD (Least Significant Digit)
Algorytm dla LSD Radix Sort1. Przydziel pamięć na tablicę z sortowanymi danymi.2. Utwórz r kolejek, digit_queue[i] zawiera dane, które mają cyfrę i na aktualnie
analizowanej pozycji.3. Badaj cyfry danych począwszy od LSD (dm) a skończywszy na MSD (d1).
Umieszczaj dane w kolejce odpowiadającej wartości cyfry.4. (Pętla zewnętrzna). Dla i = 1, 2, ... m, wykonaj kroki 5. i 6.5. (Pętla wewnętrzna 1). Dla j = 0, 1, ... (n-1), wykonaj kroki 5.1. i 5.2.
5.1. Dla A[j] weź cyfrę ostatnią (LSD) w pierwszym kroku (i=1),przedostatnią w drugim kroku (i=2) itd., aż do cyfry pierwszej (MSD) wostatnim kroku (i=m).
5.2. Wstaw A[j] na koniec kolejki związanej z wartością pobranej cyfry.6. (Pętla wewnętrzna 2). Dla qindex = 0, 1, ... (r-1), wpisz zawartości kolejek
digit_queue [qindex] do tablicy A.
Przykład sortowania dla A = {33, 60, 5, 15, 25, 12, 45, 70, 35, 7}
Krok #1digit_queue[0] 60 70
digit_queue[2] 12
digit_queue[3] 33
digit_queue[5] 5 15 25 45 35
digit_queue[7] 7
A = {60, 70, 12, 33, 5, 15, 25, 45, 35, 7}
Krok #2digit_queue[0] 5 7
digit_queue[1] 12
digit_queue[2] 25
digit_queue[3] 33 35
digit_queue[4] 45
digit_queue[6] 60
digit_queue[7] 70
AISD str. QQ
43
A = {5, 7, 12, 15, 25, 33, 35, 45, 60, 70}
Złożoność obliczeniowa algorytmu to O(m(n+r)). m zależy od r i od największej (co domodułu) wartości klucza. Dla tych samych danych różne wartości podstawy liczby dają różnewydajności.
Radix Exchange Sort (sortowanie według cyfr z wymianą)
Zasada polega na grupowaniu danych według cyfr poczynając od MSD. Potem w grupachsortujemy wg następnej cyfry itd.Algorytm dla Binary Exchange Radix Sort1. Szukaj od prawej danych z kluczem, którego pierwszy bit to 1.2. Szukaj od lewej danych z kluczem, którego pierwszy bit to 0.3. Wymień elementy i kontynuuj aż do zrównania wskaźników.4. W podzielonych zbiorach sortuj rekursywnie wg kolejnego bitu.
#include <stdio.h>
#define TRUE 1
#define FALSE 0
typedef unsigned DATA_TYPE;
class Sort {
private:
DATA_TYPE *A; // Array list A[]
int n; // size of A
DATA_TYPE extract_bits (unsigned key, int bits, int offset);
void radix_exch_sort1 (int First, int Last, int bitnum);
public:
Sort (int size) { A = new DATA_TYPE[n=size]; }
~Sort() { delete []A; }
void build_list (DATA_TYPE input[]);
void print_ary_list(int first, int last);
void Radix_Exch_Sort (DATA_TYPE input[], int bitnum);
};
typedef DATA_TYPE ARY_LIST[];
void Sort::build_list (ARY_LIST input)
{
for (int i = 0; i < n; i++)
A[i] = input[i];
}
void Sort::print_ary_list(int first, int last)
{
for (int i = first; i <= last; i++)
printf("%2d ", A[i]);
}
DATA_TYPE Sort::extract_bits (unsigned key, int bits, int offset)
{
AISD str. RR
44
return (key >> offset) & ~(~0 << bits);
}
void Sort::radix_exch_sort1 (int First, int Last, int bitnum)
{
DATA_TYPE First_bitval, Last_bitval, swap_area;
if (First < Last && bitnum >= 0) {
int i = First;
int j = Last;
while (i != j) { // scanning loop
while (TRUE) {
First_bitval = extract_bits(A[i], 1, bitnum);
if (First_bitval == 0 && i < j)
i += 1;
else break;
}
while (TRUE) {
Last_bitval = extract_bits(A[j], 1, bitnum);
if (Last_bitval != 0 && j > i)
j -= 1;
else break;
}
swap_area = A[j];
A[j] = A[i];
A[i] = swap_area;
} // End of scanning loop
if (extract_bits(A[Last], 1, bitnum) == 0)
j += 1;
printf(" -----------------------------------\n");
printf(" bit = %d | ", bitnum);
print_ary_list (First, j - 1);
printf(" | ");
print_ary_list (j, Last);
printf("\n");
radix_exch_sort1 (First, j - 1, bitnum - 1);
radix_exch_sort1 (j, Last, bitnum - 1);
}
}
void Sort::Radix_Exch_Sort (ARY_LIST input, int bitnum)
{
build_list (input);
printf ("\n List to be sorted %s:\n in ascending order");
print_ary_list (0, n - 1);
printf("\n");
radix_exch_sort1 (0, n - 1, bitnum);
printf ("\n List sorted using %s \n radix exchange sort:");
printf("\n ** OOP IMPLEMENTATION OF %s", "RADIX EXCHANGE SORT **");
radix_srt_obj.Radix_Exch_Sort (A, 8);
}
Shell Sort
W Shell Sort elementy tablicy wpisujemy do k podtablic (elementy wybierane są z krokiem k),sortujemy podtablice i kolejno wpisujemy do tablicy pierwsze elementy podtablic, drugie itd.Następnie zmniejszamy k i powtarzamy algorytm aż k = 1.
Algorytm:1. Wybierz wartość kroku k.2. Podziel tablicę A na k podtablic tak, aby każda podtablica (j) zawierała elementy o
indeksach j+i*k.3. Posortuj wszystkie podtablice (i zapisz do A - de facto te podtablice są zawarte w
A).4. Zmniejsz k według jakiejś reguły.5. Powtarzaj kroki 2. - 4. aż k = 1.6. Tablica jest posortowana.Przykład
A = [33, 60, 5, 15, 25, 12, 45, 70, 35, 7]
indx: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Krok #1 dla k = 5
1. [33, 12] -> [12, 33]
2. [60, 45] -> [45, 60]
3. [ 5, 70] -> [ 5, 70]
4. [15, 35] -> [15, 35]
5. [25, 7] -> [ 7, 25]
A = [12, 45, 5, 15, 7, 33, 60, 70, 35, 25]
indx: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Krok #2 dla k = 3
1. [12, 15, 60, 25] -> [12, 15, 25, 60]
2. [45, 7, 70] -> [ 7, 45, 70]
3. [ 5, 33, 35] -> [ 5, 33, 35]
A = [12, 7, 5, 15, 45, 33, 25, 60, 70, 35]
indx: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Krok #3 dla k = 1
A = [ 5, 7, 12, 15, 25, 33, 35, 45, 60, 70]
indx: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
#include <stdio.h>
AISD str. TT
46
class Sort {
private:
typedef int DATA_TYPE;
typedef DATA_TYPE ARY_LIST[];
DATA_TYPE *A; // Array list A[]
int n; // size of A
public:
int iter; // iteration
Sort (int size) {iter = 0;A = new DATA_TYPE[n=size];}
printf ("\n Number of %s sort is %d \n","iterations required for",
shel_srt_obj.iter);
}
Złożoność obliczeniowa dla dużych n w najgorszym przypadku wynosi od O(n1.25) doO(1.6n1.25) [dane eksperymentalne]. Można tak dobrać sekwencję kroków k, że złożonośćobliczeniowa w najgorszym przypadku wynosi O(n(log2n)2).
(A) Proste:1. Liniowe wyszukiwanie w tablicy.2. Liniowe wyszukiwanie w liście.3. Liniowe wyszukiwanie w tablicy uporządkowanej.
AISD str. VV
48
4. Liniowe wyszukiwanie w liście uporządkowanej.
(B) Zaawansowane:1. Binarne wyszukiwanie w tablicy uporządkowanej.2. Interpolacyjne wyszukiwanie w tablicy uporządkowanej.3. Wyszukiwanie z podziałem Fibonacciego.4. Wyszukiwanie w drzewie binarnym5. Zastosowania funkcji mieszającej (ang. hashing)
bin_srch_obj.print_list ("Searching for 45 in sorted array list:");
INDEX i = bin_srch_obj.Binary_Search (45);
if (i != -1)
printf("\n Search data: A[%i] = %d \n", i, A[i]);
}
Złożoność obliczeniowa przeszukiwania binarnego wynosi O(log2n) dla najgorszego przypadku.Wyszukiwanie z podziałem Fibonacciego
Wyszukiwanie z podziałem Fibonacciego jest co do zasady zbliżone do wyszukiwaniabinarnego, z tym że podział jest dokonywany wg elementów ciągu Fibonacciego. Na przykładtablica o rozmiarach 55 jest dzielona na dwie części o 21 i 34 elementach.
#include <stdio.h>
const int NOT_FOUND = -1;
class Search {
private:
typedef int DATA_TYPE;
typedef int INDEX;
typedef DATA_TYPE ARY_LIST[];
DATA_TYPE *A; // Array list A[]
int n; // size of A
public:
Search (int size) { A = new DATA_TYPE[n=size]; }
~Search() { delete []A; }
void build_list (DATA_TYPE input[]);
void print_list (char *hdr);
INDEX Fibonacci_Search (DATA_TYPE srch_key);
};
INDEX Search::Fibonacci_Search (DATA_TYPE srch_key)
{
INDEX Fib_low = 0, Fib_high = 1, offset = 0, location, N = n;
printf("\n\n Node: %d is found in the BST\n", srch_ptr->data);
}
AISD str. BBB
54
Zastosowania funkcji mieszającej (ang. hashing)
Wyszukiwanie danych z wykorzystaniem funkcji mieszającej (ang. hashing function) wymagastosowania odpowiednich struktur danych tzw. tablic podziału z porcjami (ang. hash table withbuckets) lub z listami (ang. hash table with linked lists (chains)).
Strategia funkcji mieszającej polega na takim skonstruowaniu struktur danych, w którymmożliwy jest dostęp swobodny. Element przechowywany w tablicy podziału musi posiadaćklucz, według którego jest w tej tablicy wyszukiwany (i składowany).Tablica podziału ma pewną liczbę porcji (ang. bucket), np. HT_SIZE. Wtedy mamyhash_table[0], hash_table[1], ... , hash_table[HT_SIZE-1]. Każda porcja zawiera p pozycji (ang.slot), z których każda może przechowywać daną (ang. record).
Wyróżnia się dwa typy tablic podziału:
1. Tablice podziału z porcjami2. Tablice podziału z listami (łańcuchami)
Metody stosowane dla tablicy podziału
Skonstruuj tablicę podziału.Usuń tablicę podziału.Wstaw daną z kluczem do tablicy podziału.Wyszukaj daną z kluczem w tablicy podziału.Usuń daną z kluczem z tablicy podziału.Drukuj daną z kluczem z tablicy podziału.
Aby zrealizować powyższe metody musi istnieć funkcja odwzorowująca klucz danej na adres wtablicy podziału i jest to właśnie funkcja mieszająca (ang. hashing function). W wynikustosowania funkcji mieszającej dostaje się adres porcji i tam na odpowiedniej pozycjiwykonujemy operację na danej.
Funkcje mieszające
1. Funkcja modulo dla liczb całkowitychtypedef unsigned HASH_VALUE;
typedef unsigned KEY_TYPE;
HASH_VALUE Modulus_Hash_Func (KEY_TYPE key)
{
return (key % HT_SIZE)
}
Zalecanym rozmiarem tablicy podziału jest liczba pierwsza wyrażona wzorem (4i+3). Naprzykład dla liczb z zakresu 0 .. 999 dobrym rozmiarem tablicy podziału jest 127 ( = 4*31 + 3 ).
AISD str. CCC
55
2. Funkcja modulo dla danych (np. ciągów znaków) reprezentowanych przezwskaźnik
HASH_VALUE Mod_Ptr_Hash_Func (KEY_TYPE *key)
{
return ( ((unsigned) key) % 0xFF)
}
Funkcje mieszające wykorzystujące operator modulo są dość efektywne. Dla niewielkiegozbioru liczb całkowitych wystarczy wybieranie ustalonej cyfry jako rezultatu funkcjimieszającej.
3. Łamanie liczb całkowitych (ang. Folding Hash Function)Łamanie liczb całkowitych polega na wybraniu kilku najbardziej znaczących cyfr i kilkunajmniej znaczących cyfr klucza oraz wykonaniu na nich jakiejś operacji. Metoda ta dobrze(równomiernie) rozrzuca liczby w tablicy podziału.typedef unsigned long KEY_TYPE;
HASH_VALUE Fold_Integer_Hash (KEY_TYPE key)
{
return ( (key / 10000 + key % 10000) % HT_SIZE)
}
Weźmy key = 87629426 i HT_SIZE = 251
(87629426 / 10000 + 87629426 % 10000 ) % 251 =
(8762 + 9426) % 251 = 116
4. Łamanie dla ciągu znakówHASH_VALUE Fold_String_Hash (char *key)
{
unsigned sum_ascii_value = 0;
while ( *key != '\0' ) {
sum_ascii_value += *key++;
return (sum_ascii_key % HT_SIZE);
5. Operacje bitoweW trakcie umieszczania danych w tablicach podziału może dochodzić do kolizji, gdy dlaróżnych danych generowane są takie same adresy. Jeśli w porcji jest kilka pozycji, to są onezapełniane kolejno. W przypadku listy do porcji dołączany jest kolejno wprowadzany element.Dla porcji o 1 pozycji rozwiązywanie kolizji może polegać na obliczaniu kolejnego adresu, ażdo wyczerpania wszystkich możliwości.Liniowe przeszukiwanie (liniowe badanie) (ang. linear search method, linear probing, linearopen addressing) polega na wyliczaniu kolejnego indeksu w cyklicznej tablicy podziału.
circular index = (circular index + wartość f. mieszającej) % (rozmiar tablicy podziału)
#include <stdio.h>
const int Empty = ' ';
typedef unsigned KEY_TYPE;
typedef unsigned HASH_VALUE;
typedef unsigned HASH_INDEX;
typedef char DATA_TYPE;
class Hash_Single_Bucket {
AISD str. DDD
56
private:
typedef struct DATA_RECORD {
DATA_TYPE data;
KEY_TYPE key;
} SINGLE_SLOT_BUCKET;
// SINGLE_SLOT_BUCKET circ_hash_table[HT_SIZE];
int HT_SIZE;
SINGLE_SLOT_BUCKET *circ_hash_table;
void Init_Hash_Tab (void);
public:
Hash_Single_Bucket(int table_size);
~Hash_Single_Bucket();
HASH_VALUE Hash_Function (KEY_TYPE key);
int Hash_Linear_Probe_and_Search (KEY_TYPE srch_key);
printf ("\n Hash Table with %s \n", Single-Slot Buckets");
for (int i = 0; i < HT_SIZE; i++)
printf ("\n Hash_Tabl[%d] = %c", i,
circ_hash_table[i].data);
}
void main ()
{
Hash_Single_Bucket ht_bucket_obj(10);
ht_bucket_obj.Build_Single_Bucket_Hash_Table();
ht_bucket_obj.Print_Hash_Table();
printf ("\n");
}
Rozwiązywanie kolizji w tablicach podziału z porcjami o kilku pozycjach.Analiza wprowadzania i wyszukiwania elementów w tablicy podziału z listami (łańcuchami).
if ((srch_ptr = Search_Hash_Chain_Node (k)) != NULL) {
printf("\n Hash chain search succeeds.");
printf("\n Element's key = %u", srch_ptr->key);
printf("\n Element's data = %c \n", srch_ptr->data);
}
}
void main(void)
{
Hash_Chain hc_obj(5);
printf("\n ** HASH SEARCH IN A HASH %s **", "CHAINING STRATEGY");
hc_obj.Create_Chained_Hash_Table();
hc_obj.Retrieve_Hash_Chain_Node (12);
hc_obj.Retrieve_Hash_Chain_Node (44);
hc_obj.Delete_Hash_Chain_Node (28);
hc_obj.Retrieve_Hash_Chain_Node (28);
hc_obj.Retrieve_Hash_Chain_Node (11);
}
Kryteria wyboru metod wyszukiwania, sortowania oraz złożoność obliczeniowa dla tych metod.
AISD str. III
61
DRZEWA
Def. Drzewiasta struktura danychStruktura danych jest drzewiasta gdy posiada dokładnie jeden obiekt początkowy, a dlakażdego obiektu poza początkowym istnieje w strukturze dokładnie jeden poprzednik.
Poprzednik obiektu-węzła w drzewie zwany jest przodkiem lub ojcem, a następnik potomkiemlub dzieckiem. Następniki tego samego ojca nazywa się rodzeństwem. Obiektybeznastępnikowe nazywa się liśćmi, a adres obiektu początkowego w drzewie mianuje siękorzeniem. Na rysunkach przedstawiających drzewa korzeń rysuje się zwyczajowo na górze, aliście na dole.
Stopień węzła w drzewie, to liczba jego dzieci, a stopień drzewa to maksymalny stopień jegowęzłów. Liść ma zawsze stopień 0.
Podrzewem dla danego węzła/obiektu jest potomek tego węzła wraz ze wszystkimipoddrzewami tego potomka.
Ścieżką w drzewie jest taki ciąg węzłów, że dla każdych dwóch kolejnych węzłów pierwszy znich jest przodkiem drugiego. Długość ścieżki to liczba węzłów w ciągu. Istotą drzewa jest to,że z obiektu początkowego do każdego liścia biegnie dokładnie jedna ścieżka.
Poziomem węzła-obiektu jest długość ścieżki od obiektu początkowego do tego węzła. Poziomwęzła wskazywanego korzeniem jest równy jeden. Wysokością drzewa/poddrzewa maksymalnawartość poziomu dla węzłów w drzewie. Drzewo z jednym węzłem ma więc wysokość 1,natomiast puste "drzewo" ma wysokość 0.
Drzewo jest uporządkowane, gdy kolejność dzieci dla każdego potomka jest istotna orazwartości zawarte w węzłach spełniają określone warunki. Rozpatruje się drzewa uporządkowanewedług wartości klucza identyfikującego informację przechowywaną w drzewie. Typ klucza jestzwykle typem numerycznym.
Drzewo jest zrównoważone, czy wyważone według pewnego kryterium, gdy dla każdego węzłaspełniony jest określony warunek wyważenia.
Wyróżnia się trzy metody przeglądanie drzewa binarnego to jest takiego drzewa, w którym dlakażdego obiektu występują dwa poddrzewa:1. Preorder traversal (depth_first) [NLR]
odczytaj korzeń [Node]odczytaj lewe poddrzewo, jeśli jest [Left]odczytaj prawe poddrzewo, jeśli jest [Right]
2. Inorder traversal (symetric traversal) [LNR]odczytaj lewe poddrzewo, jeśli jest [Left]odczytaj korzeń [Node]prejrzyj prawe poddrzewo, jeśli jest [Right]
AISD str. JJJ
62
3. Postorder traversal [LRN]odczytaj lewe poddrzewo, jeśli jest [Left]odczytaj prawe poddrzewo, jeśli jest [Right]odczytaj korzeń [Node]
void Binary_Tree::search_node(DATA_TYPE srch_key){ TREE_PTR srch_ptr = search_node1(root_ptr, srch_key); if (srch_ptr != NULL) printf("\n The specified node is found in tree.\n"); else
AISD str. MMM
65
printf("\n The specified node is not in tree.\n");}void Binary_Tree::Preorder1 (TREE_PTR tree_ptr){ if (tree_ptr != NULL) { // Visit root, i.e., print root's data printf(" %c ", tree_ptr->data); Preorder1(tree_ptr->left_childptr); Preorder1(tree_ptr->right_childptr); }}void Binary_Tree::Postorder1 (TREE_PTR tree_ptr){ if (tree_ptr != NULL) { Postorder1(tree_ptr->left_childptr); Postorder1(tree_ptr->right_childptr); // Visit root node (i.e., print node data) printf(" %c ", tree_ptr->data); }}void Binary_Tree::Inorder1 (TREE_PTR tree_ptr){ if (tree_ptr != NULL) { Inorder1(tree_ptr->left_childptr); // Root of the subtree, pointed to by 'tree_ptr', is visited printf(" %c ", tree_ptr->data); Inorder1(tree_ptr->right_childptr); }}void main(void){ Binary_Tree btree_obj; printf("\n OBJECT-ORIENTED IMPLEMENTATION %s \n", "OF BINARY TREE"); btree_obj.build_tree ("JUTHIKA"); printf("\nBinary Tree after PreOrder traversal:\n"); btree_obj.Preorder(); printf("\nBinary Tree after PostOrder traversal:\n"); btree_obj.Postorder(); printf("\nBinary Tree after InOrder traversal:\n"); btree_obj.Inorder(); printf("\n");}
Drzewo o dowolnej liczbie potomków dla każdego rodzica:
#include <stdio.h>#include <stdlib.h>#include <ctype.h>typedef int BOOLEAN;typedef char DATA_TYPE;#include "bc_tree.h" // For base class "Tree"class General_Tree : public Tree { private: typedef struct GTREE_NODE { DATA_TYPE data; GTREE_NODE *first_childptr; GTREE_NODE *sibling_listptr; } *TREE_PTR; TREE_PTR root_ptr; TREE_PTR create_gtree_node (DATA_TYPE new_data); void init_gtree (void) { root_ptr = NULL; } void insert_node_in_gtree (TREE_PTR parent_ptr, DATA_TYPE new_data);
AISD str. NNN
66
void gtree_preorder (TREE_PTR tree_ptr); void print_gtree_sibl (TREE_PTR tree_ptr); TREE_PTR get_root() { return (root_ptr); } public: General_Tree() { init_gtree(); } ~General_Tree(); BOOLEAN is_empty(void) {return(root_ptr == NULL);} void build_tree (DATA_TYPE A[]); void add_node(DATA_TYPE node_data) { insert_node_in_gtree(root_ptr, node_data); } void search_node(DATA_TYPE srch_key) {}; void delete_node(DATA_TYPE srch_key) {}; void print_tree(void) { printf("\n\nNODE: CHILDREN WITH SIBLINGS"); print_gtree_sibl(root_ptr); } void preorder(void) { printf("\nGeneral tree after PreOrder traversal:\n"); gtree_preorder(root_ptr); }};General_Tree::~General_Tree(){ // Use Postorder traversal method. Left as an exercise}General_Tree::TREE_PTR General_Tree::create_gtree_node(DATA_TYPE new_data){ TREE_PTR new_ptr = new GTREE_NODE; if (new_ptr != NULL) { new_ptr->data = new_data; new_ptr->first_childptr = NULL; new_ptr->sibling_listptr = NULL; } return (new_ptr); // NULL if alloc fails.}void General_Tree::insert_node_in_gtree (TREE_PTR parent_ptr, DATA_TYPE new_data){ // If the general tree is empty, insert it. if ((parent_ptr == root_ptr) && (root_ptr == NULL)) root_ptr = create_gtree_node (new_data); else if (parent_ptr == NULL) printf ("\ninsert_node_in_gtree: Parent %s \n", "pointer is invalid"); else { TREE_PTR new_ptr = create_gtree_node (new_data); if (parent_ptr->first_childptr == NULL) parent_ptr->first_childptr = new_ptr; else { TREE_PTR sibling_ptr = parent_ptr->first_childptr; // Add at the end of the sibling list. while (sibling_ptr->sibling_listptr != NULL) // Advance to the next sibling (brother or sister) sibling_ptr = sibling_ptr->sibling_listptr; sibling_ptr->sibling_listptr = new_ptr; } }}void General_Tree::build_tree (DATA_TYPE A[]){ // Root = G; its children = E, N, E insert_node_in_gtree (root_ptr, 'G'); insert_node_in_gtree (root_ptr, 'E'); insert_node_in_gtree (root_ptr, 'N'); insert_node_in_gtree (root_ptr, 'E'); // Children of 'E' are: R, A, L
AISD str. OOO
67
insert_node_in_gtree (root_ptr->first_childptr, 'R'); insert_node_in_gtree (root_ptr->first_childptr, 'A'); insert_node_in_gtree (root_ptr->first_childptr, 'L');}void General_Tree::gtree_preorder(TREE_PTR gtree_ptr){ if (gtree_ptr != NULL) { printf ("%c ", gtree_ptr->data); if (gtree_ptr->sibling_listptr != NULL) gtree_preorder (gtree_ptr->sibling_listptr); if (gtree_ptr->first_childptr != NULL) gtree_preorder (gtree_ptr->first_childptr); }}void General_Tree::print_gtree_sibl(TREE_PTR tree_ptr){ TREE_PTR curr_ptr = tree_ptr; static int n = 0; if (root_ptr == NULL) printf ("\n General tree is empty. \n"); while (curr_ptr != NULL) { if (n > 0) { if (isalnum (curr_ptr->data)) printf(" %c->", curr_ptr->data); } n++; print_gtree_sibl (curr_ptr->sibling_listptr); if (isalnum (curr_ptr->data)) printf("\n %c : ", curr_ptr->data); curr_ptr = curr_ptr->first_childptr; }}void main(void){ General_Tree gtree_obj; static DATA_TYPE *A = "GENERAL"; printf ("\n *** OOP IMPLEMENTATION OF GENERAL TREE %s", "***\n USING CHILD-SIBLING RELATIONSHIP \n"); gtree_obj.build_tree(A); gtree_obj.preorder(); gtree_obj.print_tree(); printf("\n"); delete A;}
Def. Binarne drzewo uporządkowaneBinarne drzewo uporządkowane (ang. Binary Search Tree), to drzewiasta struktura danych zokreślonymi dwoma rodzajami powięzań: lewym (l) i prawym (r) oraz z wartością klucza (k)określoną tak dla każdego obiektu ob, że:
ob.l != null => klucze w poddrzewie ob.l są mniejsze od ob.koraz ob.r != null => klucze w poddrzewie ob.r są większe od ob.k
Przykład drzewa binarnego uporządkowanego.
8
9 4 8
8
104
9
AISD str. PPP
68
Typ z drzewiastą strukturą danych odpowiada kolekcji kluczy bez możliwościprzechowywania wielokrotnych kopii wartości elementów (podobnie jak w zbiorach).
#include <stdio.h>typedef int BOOLEAN;typedef int DATA_TYPE; // Type of node's data#include "bc_tree.h" // For base class "Tree"class Binary_Search_Tree : public Tree { private: typedef struct BSTREE_NODE { DATA_TYPE data; BSTREE_NODE *left_childptr; BSTREE_NODE *right_childptr; } *TREE_PTR; TREE_PTR root_ptr; // root of the BST int INPUT_SIZE; void init_BSTree() { root_ptr = NULL; } void delete_BST (TREE_PTR& tree_ptr); TREE_PTR search_node_in_BST(TREE_PTR tree_ptr, DATA_TYPE srch_key); void traverse_BST_Postorder(TREE_PTR tree_ptr); void traverse_BST_Preorder(TREE_PTR tree_ptr); void traverse_BST_Inorder(TREE_PTR tree_ptr); TREE_PTR get_root() { return root_ptr; } public: Binary_Search_Tree(int size) {init_BSTree(); INPUT_SIZE = size;} ~Binary_Search_Tree(); BOOLEAN is_empty(void) {return (root_ptr == NULL);} void build_tree(DATA_TYPE A[]); void add_node(DATA_TYPE new_data); void search_node(DATA_TYPE srch_key); void delete_node(DATA_TYPE srch_key) {}; void print_tree(void); void postorder(void); void preorder(void); void inorder (void);}; Binary_Search_Tree::~Binary_Search_Tree(){ delete_BST (root_ptr); }void Binary_Search_Tree::add_node(DATA_TYPE new_data){ TREE_PTR new_ptr, target_node_ptr; new_ptr = new BSTREE_NODE; new_ptr->data = new_data; new_ptr->left_childptr = NULL; new_ptr->right_childptr = NULL; if (root_ptr == NULL) root_ptr = new_ptr; else { TREE_PTR tree_ptr = root_ptr; while (tree_ptr != NULL) { target_node_ptr = tree_ptr; if (new_data == tree_ptr->data) return; else if (new_data < tree_ptr->data) tree_ptr = tree_ptr->left_childptr; else // new_data > tree_ptr->data tree_ptr = tree_ptr->right_childptr; } // end of while (tree_ptr != NULL) if (new_data < target_node_ptr->data) target_node_ptr->left_childptr = new_ptr;
AISD str. QQQ
69
else // insert it as its right child target_node_ptr->right_childptr = new_ptr; }}void Binary_Search_Tree::add_node(DATA_TYPE new_key){ add_node(tree_ptr, new_key);}void Binary_Search_Tree::add_node(TREE_PTR& current, DATA_TYPE new_key){ if (current == NULL) { current = new BSTREE_NODE; if (current == NULL) return; //Brak pamieci current->data = new_key; current->left_childptr = NULL; current->right_childptr = NULL; } else if (current->data < new_key) { add_node(tree_ptr->right_childptr, new_key); } else if (current->data > new_key) { add_node(tree_ptr->left_childptr, new_key); }}void Binary_Search_Tree::build_tree(DATA_TYPE A[]){ for (int j = 0; j < INPUT_SIZE; j++) add_node (A[j]);}
AISD str. RRR
70
Binary_Search_Tree::TREE_PTRBinary_Search_Tree::search_node_in_BST (TREE_PTR tree_ptr, DATA_TYPE Key){ if (tree_ptr != NULL) { if (Key == tree_ptr->data) return (tree_ptr); else if (Key < tree_ptr->data) search_node_in_BST(tree_ptr->left_childptr, Key); else // (Key > tree_ptr->data) search_node_in_BST(tree_ptr->right_childptr, Key); } else { printf("\n search_node_in_BST: Node %d %s \n", Key, "is not found"); return (NULL); }}void Binary_Search_Tree::search_node(DATA_TYPE srch_key){ TREE_PTR srch_ptr = NULL; srch_ptr = search_node_in_BST(root_ptr, srch_key); if (srch_ptr != NULL) printf("\n\n Node: %d is found in the BST\n", srch_ptr->data);}void Binary_Search_Tree::delete_BST(TREE_PTR& tree_ptr){ if (tree_ptr != NULL) { delete_BST (tree_ptr->left_childptr); delete_BST (tree_ptr->right_childptr); delete tree_ptr; tree_ptr = NULL; }}void Binary_Search_Tree::traverse_BST_Preorder (TREE_PTR tree_ptr){ if (tree_ptr != NULL) { printf(" %d ", tree_ptr->data); traverse_BST_Preorder(tree_ptr->left_childptr); traverse_BST_Preorder(tree_ptr->right_childptr); }}void Binary_Search_Tree::preorder(void){ printf("\n The Binary Search Tree after %s \n", "PreOrder traversal:"); traverse_BST_Preorder(root_ptr);}void Binary_Search_Tree::traverse_BST_Postorder (TREE_PTR tree_ptr){ if (tree_ptr != NULL) { traverse_BST_Postorder(tree_ptr->left_childptr); traverse_BST_Postorder(tree_ptr->right_childptr); printf(" %d ", tree_ptr->data); }}
void Binary_Search_Tree::postorder(void){ printf("\n The Binary Search Tree after %s \n", "PostOrder traversal:"); traverse_BST_Postorder(root_ptr);}
W zależności od kształtu drzewa binarnego rząd kosztu dla algorytmu przeszukiwania drzewajest mocno zróżnicowany tj. od O(ln n) do O(n). Aby zminimalizować koszt stosuje się wpraktyce drzewa wyważone.
Waga dla obiektu ob w drzewie jest określona wzorem: weight(ob) = 0 jeśli ob==null
weight(ob) = 1+weight(ob.l)+weight(ob.r) jeśli ob!=nullWysokość dla obiektu ob w drzewie jest określona wzorem:
height(ob)= 0 jeśli ob==nullheight(ob)= 1+height(ob.l) jeśli ob!=null,height(ob.r)<=height(ob.l)
lub 1+height(ob.r) jeśli ob!=null,height(ob.r)>height(ob.l)Drzewo jest dokładnie wyważone jeśli dla każdego obiektu ob wagi ob.l, ob.r są równe lubróżnią się o jeden. O(ln n)Drzewo jest wyważone ze względu na wysokość jeśli dla każdego obiektu obwysokości ob.l, ob.r są równe lub różnią się o jeden. O(ln n)Drzewo jest wyważone wagowo jeśli dla każdego obiektu ob:
Drzewo jest zdegenerowane gdy każdy jego obiekt ma co najwyżej jeden następnik. O(n)
AISD str. TTT
72
Drzewo stworzone przy równomiernym rozkładzie wstawianych kluczy ma funkcję kosztu dlaoperacji przeszukania rzędu O(ln n). Dokładne wyważenie takiego drzewa daje dla dużychwartości n skrócenie drogi poszukiwań co najwyżej do 72%.
Równoważenie obiektów binarnego drzewa uporządkowanego zakowowano w językuspecyfikacji podobnym do C++. W języku tym istnieje bogatszy mechanizm parametryzacjitypów danych i dostęp do jest ograniczony - kontrolowany przez system. Nie używa się newani delete. Zamiast strzałek przy zmiennych wskaźnikowych (dla wszystkich obiektówstruct) stosuje się kropki.
class TreeIns<Key> { //Drzewo binarne z kluczamipublic: Tree(); //Tworzenie pustego drzewa void ins(Key k); //Wstawianie węzła do drzewa ~Tree(); //Niszczenie drzewaprotected: struct TNode { //Typ obiektu - elementu drzewa
if (ob == null) return null; //Jeśli zły adres if (ob.r == null) { //Jeśli ob największy to: TNode ret_ob = ob; // zapamiętaj wartość do zwrócenia if (ob.r == null) ob = null; //Wyłączenie z drzewa else ob = ob.r return ret_ob; //Zwróć odcięty } else rem_max(ob.r); //Szukaj dalej// if (ob != null) balance_height(ob); //Wyważaj}void rem(TNode &ob, Key k) { if (ob == null) return; //Brak klucza w drzewie: koniec if (k < ob.k) rem(ob.l, k);//Gdy klucz mniejszy od bieżącego
// idź do lewego poddrzewa else if (ob.k < k) rem(ob.r, k); //Gdy klucz większy od
// bieżącego idź do prawego else if (ob.l == null) ob = ob.r;//Gdy równy i najmniejszy w
//poddrzewie usuń najmniejszy } else { //Znaleziony: trzeba go zastąpić TNode tmp = rem_max(ob.l); //Znajdź najbliższy mniejszy tmp.l = ob.l; //Wstaw najbliższy w miejsce ob tmp.r = ob.r; ob = tmp;};};void rem(Key k) { rem(root, k); }};
Rysunek obrazujący usuwanie klucza o wartości 9.
DRZEWA AVL
Drzewa AVL (od nazwisk Adel'son-Vel'skii i Landis) są uporządkowanymi drzewamibinarnymi, które się wywa ze względu na wysokość. Złożoność obliczeniowa algorytmuposzukiwania jest O(ln n) niezależnie od kolejności wstawioania kluczy.
class TreeRot<Key>: TreeRem<Key> {public:void r_rot(TNode &ob) { //Prawa rotacja TNode new_ob = ob.l; //Obiekt, który będzie "na szczycie" ob.l = new_ob.r; //Zmiana wskaźników
new_ob.r = ob; ob = new_ob;}void l_rot(TNode &ob) { //Lewa rotacja TNode new_ob = ob.r; //Obiekt, który będzie "na szczycie" ob.r = new_ob.l; //Zmiana wskaźników new_ob.l = ob; ob = new_ob;}void rr_rot(TNode &ob) { //Podwójna prawa rotacja l_rot(ob.l); // lewa rot. na lewym poddrzewie r_rot(ob); // i prawa rotacja
7
9
1047
104
AISD str. VVV
74
}void ll_rot(TNode &ob) { //Podwójna lewa rotacja r_rot(ob.r); // prawa rot. na prawym poddrzewie l_rot(ob); // i lewa rotacja}void balance_height(TNode &ob) { if (ob == null) return; Nat ob_l_h = height(ob.l);//Obliczenie wysokości poddrzew Nat ob_r_h = height(ob.r); if (ob_l_h > ob_r_h+1) {//Czy rotacja przesuwająca obiekty z
// lewa na prawo? if (height(ob.l.l) > height(ob.l.r)) {
// obiekty z prawa na lewo? if (height(ob.r.r) > height(ob.r.l)) {
//Czy pojedyncza rotacja?l_rot(ob);
} else {ll_rot(ob);
}} }; O(n)->O(1)
//Wersja zoptymalizowana poniżej i - patrz Wirth, Algorytmy +...
rr_rotr_rot
AISD str. WWW
75
Należy rozpatrzyć 4 istotne warunki nierównowagi dla węzła X, którego współczynnikwyważenia = 2, -2.
1. W lewym poddrzewie x lewe poddrzewo jest wyższe2. W lewym poddrzewie x prawe poddrzewo jest wyższe3. W prawym poddrzewie x prawe poddrzewo jest wyższe4. W prawym poddrzewie x lewe poddrzewo jest wyższeSytuacje 1 i 3 oraz 2 i 4 są wzajemnie symetryczne.W przypadku 1. stosujemy prawą rotację (r_rot).W przypadku 2. stosujemy podwójną prawą rotację (rr_rot).W przypadku 3. stosujemy lewą rotację (l_rot).W przypadku 4. stosujemy podwójną lewą rotację (ll_rot).
typedef int BOOLEAN; typedef int DATA_TYPE;typedef int BALANCE;#include "bc_tree.h" // For base class "Tree"class AVL_Tree : public Tree { private: typedef struct AVL_NODE { DATA_TYPE data; BALANCE bal_fact; AVL_NODE *left_childptr; AVL_NODE *right_childptr; } *TREE_PTR; TREE_PTR root_ptr; int INPUT_SIZE; int curr_node_num; // for printing int curr_line_pos; // for printing void delete_AVL_tree(TREE_PTR tree_ptr); TREE_PTR create_AVL_node(DATA_TYPE new_data); void rotate_right(TREE_PTR tree_ptr); void rotate_left (TREE_PTR tree_ptr); void printlevel(TREE_PTR root_ptr, int level, int curr_level); void show_AVL_vertically(void); int max(int a,int b) { if (a>=b) return(a); else return(b); } void traverse_AVL_Inorder(TREE_PTR tree_ptr);
void AVL_Tree::add_node(DATA_TYPE new_data){ TREE_PTR curr_ptr = root_ptr, new_ptr, // Pointer to new node most_recent_ptr = root_ptr, // Most recent node with // B.F. -1, or +1 most_recent_child_ptr = NULL, // Child of most recent // node, new_root_ptr = NULL, // New subtree root after // rebalancing
most_recents_parent_ptr = NULL, // Parent of the most // recent node parent_ptr = NULL; // Node that new node is child of. int unbal_flag, // Tree unbalanced after insertion rebal_direc; // Rebalancing direction after insertion
if (curr_ptr == NULL) { // AVL tree is empty new_ptr = create_AVL_node(new_data); root_ptr = new_ptr; } else { curr_ptr = root_ptr; while (curr_ptr != NULL) { if (curr_ptr->bal_fact != 0) { most_recent_ptr = curr_ptr; most_recents_parent_ptr = parent_ptr; } if (new_data == curr_ptr->data) return; // Data already exists
void AVL_Tree::show_AVL_vertically(void){ int treedepth = get_AVL_depth(root_ptr); printf("\n"); for (int i = 0; i < 33; i++) putchar(' '); printf("THE AVL TREE\n"); for (int level = 1; level <= treedepth ;level++) { int curr_width = 80 >> level; int prev_width = 80 >> (level -1); curr_node_num = 1; curr_line_pos = 0; if (level != 1) { for (i = 1; i<= prev_width - 1; i++) putchar(' '); for (i = 0;i <= (79 - prev_width); i++) { if (i % (2 * prev_width) == 0) putchar('|'); else putchar(' '); }
printf("\n");for (i = 1 ; i <= curr_width -1; i++) { putchar(' ');}
for (i = 0; i <= 80 - curr_width ;i++) { if (i < 2 * curr_width) putchar('-'); if ((i >= 2 * curr_width) && (i < 4 * curr_width)) putchar(' '); if ((i >= 4 * curr_width) && (i < 6 * curr_width)) putchar('-'); if ((i >= 6 * curr_width) && (i < 8 * curr_width)) putchar(' '); if ((i >= 8 * curr_width) && (i < 10 * curr_width)) putchar('-'); if ((i >= 10 * curr_width) && (i < 12 * curr_width)) putchar(' '); if ((i >=12 * curr_width) && (i < 14 * curr_width)) putchar('-'); if ((i >= 14 * curr_width) && (i < 16 * curr_width)) putchar(' '); } printf("\n"); } printlevel(root_ptr, level, 1); printf("\n"); }}
void main(void){ static DATA_TYPE A[8] = {16, 54, 14, 63, 62, 48, 21, 53}; static int no_data = sizeof(A)/sizeof(DATA_TYPE); AVL_Tree avl_tree_obj(no_data); printf("\n OBJECT-ORIENTED %s \n", "IMPLEMENTATION OF AVL TREE"); avl_tree_obj.build_tree(A); printf("\n AVL Tree after InOrder Traversal:\n"); avl_tree_obj.inorder(); avl_tree_obj.print_tree(); delete []A;}
CYFROWE DRZEWA PRZESZUKIWAŃ (ang. Radix Search Trees, Digital Search Trees)
AISD str. CCCC
81
Rozgałęzienia w cyfrowym drzewie binarnym są związane z cyfrowymi własnościami klucza (anie bezpośrednio jego wartością). Cyfrowe drzewa przeszukiwań mogą mieć klucze dyskretnelub niedyskretne (ciągłe). Pierwsze mają wartości binarne, a drugie zapisane w formaciezmiennego przecinka.Dla dyskretnych kluczy rozgałęzienie w drzewie zależy od pojedynczego znaku (cyfry, bitu).
BINARNE CYFROWE DRZEWO PRZESZUKIWAŃ (ang. Binary Digital Search Tree).
Def. Binarne cyfrowe dyskretne drzewo przeszukiwań jest drzewem binarnym z kluczem-liczbąbinarną w każdym węźle, przy czym dla każdego węzła na i spełnione są warunki:1. Wszystkie węzły w lewym poddrzewie korzenia zawierają klucze, których i-ty bit jest 02. Wszystkie węzły w prawym poddrzewie korzenia zawierają klucze, których i-ty bit jest 1.
typedef int Nodetype;
class People_Database { protected: typedef int Keytype; struct DataObject { // Member record Keytype key; // member id char *name; // name char *occupation; int age; // age int income; // yearly gross (x $1000) int years; // number years member }; DataObject *people_db; int NUMOBJ; public: People_Database(int numobj); ~People_Database(); void build_initial_db(void); DataObject *get_db() {return people_db;}};People_Database::People_Database(int numobj){ // Allocate an array of given 'numobj' people_db = new DataObject[NUMOBJ = numobj];}People_Database::~People_Database(){ delete [] people_db;}// --- Initial setup of People database ---void People_Database::build_initial_db(void){ people_db[0].key = 16; people_db[0].name = "Bob";people_db[0].occupation = "engineer"; people_db[0].age = 29;people_db[0].income = 57; people_db[0].years = 3; people_db[1].key = 54; people_db[1].name = "John";people_db[1].occupation = "student"; people_db[1].age = 24;people_db[1].income = 12; people_db[1].years = 2; people_db[2].key = 14; people_db[2].name = "Kathy";people_db[2].occupation = "doctor"; people_db[2].age = 39;
printf("The Digital Search Tree after PostOrder traversal\n");
MDST_Postorder(root_ptr);
printf("\n");
}
void main(void)
{
// Declare "People_Database" object with size=10
People_Database pdb_obj(10);
pdb_obj.build_initial_db();
// Declare an object of "M-ary Digital_Search_Tree"
// class for M = 2, MAXBITS = 6, NUMBITS = 1,
// LEVEL= MAXBITS/NUMBITS - 1.
// NOBJ = 10 (in "People_Database" object)
MDigital_Search_Tree bdstree_obj(2, 6, 1, 5, 10);
printf("\n OOP IMPLEMENTATION OF M-ARY %s \n %s \n",
"DIGITAL SEARCH TREE, M=2",
" FOR PEOPLE DATABASE");
// ** Building M-ary Digital Search Tree
bdstree_obj.build_tree(pdb_obj.get_db());
bdstree_obj.print_tree();
Drzewa B (ang. B-Trees
Drzewa B służą do przechowywania uporządkowanych danych w pamięci zewnętrznej (nadyskach).Własności drzewa B rzędu N:- Należy utrzymywać minimalną wysokość drzewa B.- Drzewo B musi być wyważone.- Wszystkie liście drzewa B pozostają na tym samym poziomie.- Nie istnieją puste poddrzewa powyżej poziomu liści.- Korzeń może nie mieć dzieci, jeśli drzewo B zawiera tylko jeden węzeł.- Każdy z węzłów zawiera co najmniej N kluczy i co najwyżej 2N kluczy. Węzeł jest
pełny jeśli zawiera 2N kluczy. Korzeń może zawierać tylko 1 klucz dla dowolnegoN
- Drzewo B jest budowane przez wstawianie klucza do liścia. Drzewo wzrasta"wszerz" i "wzwyż" na skutek podziału węzłów pełnych.
- W trakcie wprowadzania nowego elementu danych do drzewa B zachowywany jestodpowiedni porządek. Klucz wprowadzanej danej jest porównywany z kluczem(kluczami) w korzeniu i po kolei z kluczami węzłów pośrednich, aż trafia doodpowiedniego liścia (węzła X). Jeśli X jest pełen, to następuje jego podział na dwai klucz ze środkowej pozycji jest wstawiany do rodzica węzła X, co również może
AISD str. HHHH
86
spowodować podział. W ten sposób podziały mogą propagpwać aż do korzenia.Jeśli natomiast węzeł X nie jest pełny, to nowa dana jest umieszczana tak abyzachować porządek w węźle. Po wstawieniu węzła drzewo musi zachowywaćwłasności drzewa B.
- Po usunięciu klucza drzewo musi zachowywać własności drzewa B.
Przyk•ad budowy drzewa B rz•du 2
Wstawiamy element 33
33
0 0
Wstawiamy elementy 60, 5 15
5 15 33 60
0 0 0 0 0
Wstawiamy element 25
15
* *
5 25 33 60 (nie jest B)
0 0 0 0 0 0
25
* *
5 15 33 60
0 0 0 0 0 0
Wstawiamy element 12
25
* *
5 12 15 33 60
0 0 0 0 0 0 0
Wstawiamy element 45
25
* *
5 12 15 33 45 60
0 0 0 0 0 0 0 0
Wstawiamy element 70
25
* *
5 12 15 33 45 60 70
0 0 0 0 0 0 0 0 0
Wstawiamy element 35
25 45
* * *
5 12 15 33 35 60 70
0 0 0 0 0 0 0 0 0 0
AISD str. IIII
87
Wstawiamy element 7
25 45
* * *
5 7 12 15 33 35 60 70
0 0 0 0 0 0 0 0 0 0 0
W drzewie B nie można umieszczać duplikatów kluczy.Częstość podziału węzłów można zmniejszyć przez zwiększenie stopnia drzewa B.
Usuwanie klucza z drzewa B1. Usuwanie klucza z korzenia, który ma więcej niż 1 klucz i jest liściem.2. Usuwanie klucza z korzenia, który ma więcej niż 1 klucz i nie jest liściem.
Zastąpienie usuniętego klucza najmniejszym kluczem prawego poddrzewausuwanego klucza.Zastąpienie usuniętego klucza największym kluczem lewego poddrzewa usuwanegoklucza.Połączenie lewego i prawgo poddrzewa jeśli mają po N kluczy.
3. Usuwanie klucza z korzenia, który ma 1 klucz i nie jest liściem. (jak 2.)4. Usuwanie klucza z korzenia, który ma 1 klucz i jest liściem.5. Usuwanie klucza z liścia, który ma co najmniej (N+1) kluczy.6. Usuwanie klucza z liścia, który ma N kluczy.
Weź klucz od lewego sąsiada, jeśli ma on co najmniej (N+1) kluczy.Weź klucz od prawego sąsiada, jeśli ma on co najmniej (N+1) kluczy.Lewy sąsiad ma dokładnie N kluczy. Połącz węzeł bieżący (z którego usuwamyklucz) z lewym sąsiadem.Prawy sąsiad ma dokładnie N kluczy. Połącz węzeł bieżący (z którego usuwamyklucz) z prawym sąsiadem.
Przyk•ad.
25 45
* * *
5 7 12 15 33 35 60 70
0 0 0 0 0 0 0 0 0 0 0
Usuwamy w•ze• 7
25 45
* * *
5 12 15 33 35 60 70
0 0 0 0 0 0 0 0 0 0
Usuwamy w•ze• 25
15 45
* * *
5 12 33 35 60 70
0 0 0 0 0 0 0 0 0
Usuwamy w•ze• 33
45
AISD str. JJJJ
88
* *
5 12 15 35 60 70
0 0 0 0 0 0 0 0
Usuwamy w•ze• 45
35
* *
5 12 15 60 70
0 0 0 0 0 0 0
Usuwamy w•ze• 12
35
* *
5 15 60 70
0 0 0 0 0 0
Usuwamy w•ze• 15
5 35 60 70
0 0 0 0 0
Drzewa BB (ang. Binary B-Trees)
Drzewo BB (drzewo 2-3) jest drzewem B rzędu 1. Węzeł drzewa zawiera jeden klucz (K) oraz 3wiązania: lewe, prawe i poziome. Jeżeli wiązanie lewe nie jest puste, to klucze tego poddrzewasą mniejsze niż klucz K. Dla wiązań prawego i poziomego wskazywane przez nie poddrzewamają klucze większe.
1. Dla żadnego węzła w drzewie nie mogą równocześnie występować wiązaniapoziome i prawe.
2. Poziome połączenia nie występują jedno po drugim, tzn. jeśli węzeł X ma poziomepołączenie, to korzeń poddrzewa wskazanego przez nie musi mieć poziomepołączenie "puste".
3. Wysokości wszystkich następników (poddrzew) dla każdego węzła w drzewie sątakie same; przy liczeniu wysokości drzewa połączenia poziome jej nie zwiększają.
Wstawianie do drzew BB wykonuje się tak jak do drzew B, a równoważenie wykonywane jestwedług 5 schematów.Schematy równoważenia drzewa BB.
A B C A B
a B A c d a b C
1
b c a b c d
3 5
B
A B
A C
a b c
a b c d
AISD str. KKKK
89
B 4
A C
A c 2
a B d
a b
b c
Przykład. Wstawianie kluczy do drzewa BB. Klucze: A, Z, Y, X, B
1. A
2. A Z
3. A Z 4 Y
Y A Z
4. Y
A X Z
5. Y Y B Y
A X Z 4 B Z 2 A X Z
B A X
AISD str. LLLL
90
REKURSJA
Definicja. Funkcja jest rekursywna, jeśli jest zdefiniowana przy pomocy odwołań do siebie.Podobnie program jest rekursywny, jeśli wywołuje sam siebie.
Z rekursją bezpośrednią mamy do czynienia wtedy, gdy procedura wywołuje "siebie" zanim sięskończy. W rekursji pośredniej procedura wywołuje inną procedurę, która z kolei wywołujeprocedurę, która ją wywołała. Na przykład funkcja f() wywołuje g(), która wywołuje h(), a tawoła f(). Zaletą rekursji jest zwięzłość zapisu, zaś wadą może być koszt implementacjiprzejawiający się dużym zapotrzebowaniem na pamięć i dużym czasem przetwarzania (przywielu poziomach rekursji).Metoda "dziel i rządź" (ang. divide and conquer) w rozwiązywaniu problemów wiąże się częstoz rekursją. Problem do rozwiązania jest dzielony na mniejsze, aż do uzyskania rozwiązania apotem uzyskane rezultaty są wykorzystywane do obliczenia końcowego rezultatu. Istnieniewarunków końcowych jest konieczne do tego, aby rekursja nie trwała w nieskończoność. Wtypowej metodzie "dziel i rządź" dany problem rozbija się na dwie połówki, do których stosujesię powyższą metodę.
Obliczanie silni
1 dla n = 0 lub n = 1n! =
n * (n-1)! dla n > 1
#include <stdio.h>
#include <stdlib.h>
int factorial_n_recur (int n)
{
if (n < 0) {
printf("\n factorial_n_recur: %s %d \n"
"Invalid n =", n);
exit (0);
}
if ((n == 0) || (n == 1))
return (1); // anchor case
return ( n * factorial_n_recur (n - 1));
}
int factorial_n_iter (int n)
{
int fact = 1;
if (n < 0) {
printf("\n factorial_n_iter: %s %d \n",
"Invalid n =", n);
exit (0);
}
if ((n == 0) || (n == 1))
return (1);
AISD str. MMMM
91
while (n > 1)
fact *= n--; // fact = fact * n--
return (fact);
}
void main(void)
{
int n = 5;
printf ("\n ** FACTORIAL n! ** \n");
printf ("\n Recursive Version: %d! = %d \n",
n, factorial_n_recur(n));
printf (" Iterative Version: %d! = %d \n",
n, factorial_n_iter(n));
}
Obliczanie potęgi liczbyniezdefiniowane dla A=0, B=0
power (A, B) = 0 dla A=0, B>01 dla A>0, B=0AB dla A>0, B>0
#include <stdio.h>
#include <stdlib.h>
int power_recur (int a, int b)
{
if ((a == 0) && (b == 0)) {
printf ("\n power_recur: Undefined 0^0 \n");
exit (0);
}
if (a == 0)
return (0);
if ((a !=0) && (b == 0))
return (1);
if (b > 0)
return (a * power_recur (a, b - 1));
}
int power_iter (int a, int b)
{
int repeated_product = 1;
if ((a == 0) && (b == 0)) {
printf ("\n power_recur: Undefined 0^0 \n");
exit (0);
}
if (a == 0)
return (0);
if ((a !=0) && (b == 0))
return (1);
while (b > 0) {
repeated_product *= a;
b--;
}
}
void main(void)
AISD str. NNNN
92
{
int a = 3, b = 4;
printf ("\n ** POWER FUNCTION: A^B ** \n");
printf ("\n Recursive version: %d^%d = %d \n",
a, b, power_recur (a, b));
printf (" Iterative version: %d^%d = %d \n",
a, b, power_iter (a, b));
}
- Stos w wywołaniu procedur rekursywnych.
Wieże HanoiMając trzy igły należy przełożyć N (64) krążków rożnej wielkości (spoczywających na jednejigle od największego do najmniejszego)na drugą igłę korzystając z pomocniczej igły tak, aby
- w trakcie jednego ruchu przekładać tylko jeden krążek- nigdy krążek większy nie może leżeć na krążku mniejszym
printf("\n ** OOP IMPLEMENTATION OF %s **\n %s \n",
"TOWERS OF HANOI", " USING RECURSION");
hanoi_obj.build_and_init_poles();
printf("\n\n ** States of Poles before moves **\n");
hanoi_obj.draw_pole(hanoi_obj.pole_1);
hanoi_obj.draw_pole(hanoi_obj.pole_2);
hanoi_obj.solve_hanoi (max_disk_no);
printf("\n\n ** States of Poles after moves **\n");
hanoi_obj.draw_pole(hanoi_obj.pole_1);
hanoi_obj.draw_pole(hanoi_obj.pole_2);
}
Zasady stosowania rekursji
1. Niektóre problemy są z natury rekursywne i stąd ich rekursywne rozwiązanie jestnaturalne. Mogą być one rozwiązane w inny sposób, co często wymaga znaczniewiększego wysiłku w programowaniu.
2. Rekursja zwiększa czytelność rozwiązania oraz łatwość jego modyfikacji wstosunku do równoważnego rozwiązania iteracyjnego.
3. Rekursja nie jest metodą rozwiązywania wszystkich problemów. Jest kosztowana zewzględu na wykorzystywanie zasobów systemu.
4. Nie należy stosować rekursji, jesli nie można (nie potrafi się) zdefiniować warunkukońcowego (brzegowego). Brak tego warunku powoduje niekończące wywołaniaprocedury rekursywnej.
5. Rekursję powinno stosować się do problemów, które można zdefiniować przypomocy ich samych (tych problemów) mniejszych i prostszych form, prowadzącychdo warunku końcowego.
6. Rekursję można zawsze zastąpić obliczeniami iteracyjnymi z wykorzystaniem stosu.
AISD str. SSSS
97
GRAFY
Definicja. Grafem G nazywamy parę zbioru węzłów V, oraz zbioru krawędzi E, takich że dlaskończonego n
G = {V, E}, gdzie V = {V1, V2, ..., Vn}E = {Eij = (Vi, Vj), 1 <= i <= n, 1 <= j <= n }
Krawędź, (albo łuk) określa połączenie między dwoma węzłami. Krawędź nieskierowana jestdefiniowana przez parę węzłów np. (A, B). Krawędź skierowana jest określona przezuporządkowaną parę węzłów <A, B>, gdzie A jest początkiem, a B końcem krawędzi.Graf skierowany (ang. directed graph - digraph) składa się z węzłów i krawędzi skierowanych.Dwa węzły są sąsiednie, jeśli występuje między nimi krawędź. Cyklem (lub pętlą) w grafienazywamy taką ścieżkę w grafie, której początek i koniec są w tym samym węźle. Graf jestacykliczny, jeśli nie ma w nim cykli. Krawędź tworzy cykl, jeśli łączy ze sobą ten sam węzeł.Stopniem węzła w grafie (deg(A)) nazywamy liczbę krawędzi wychodzących z tego węzła.Krawędź będąca pętlą dodaje 2 do stopnia węzła. W grafie skierowanym stopniem wejściowymwęzła (ang. in-degree) (deg(A)) nazywamy liczbę krawędzi zakończonych w tym węźle. Stopieńwyjściowy (ang. out-degree) (deg+(A)) to liczba krawędzi rozpoczynających się w danym węźle.Węzeł A jest odizolowany (niepołączony) jeśli deg(A)=0. Graf jest połączony (ang. connected),jeśli nie zawiera węzłów odizolowanych. Z krawędzią może być związana wartość zwana wagą(np. koszt, odległość).Ścieżka z węzła A do węzła B w grafie skierowanym (nieskierowanym) to sekwencja krawędziskierowanych (nieskierowanych) oznaczonych przez p(A, B). Ścieżka ma długość m, jeśli w jejskład wchodzi m krawędzi (m+1 węzłów)
V0=A, V1, V2, ... Vm=B
Najkrótszą ścieżką w grafie ważonym jest ścieżka z minimalną sumą wag krawędzi.
Graf można przeglądać według dwóch metod:- przeglądanie w głąb (ang. depth-first traversal DFS)- przeglądanie wszerz (ang. breadth-first traversal BFS)
Przeglądanie grafu nie jest jednoznaczne. Może być rozpoczynane od dowolnego węzła.Zastosowania: sprawdzanie połaczenia, wyznaczanie najkrótszej (wagowo) ścieżki.Aby uniknąć wielokrotnego uwzględniania tego samego węzła, do którego można dotrzeć zróżnych stron stosuje się wskaźnik (ang. flag) informujący, czy dany węzeł był już wizytowany.Przeglądanie w głąb zwane jest również przeglądaniem z powrotami (ang. backtracking) ipolega na posuwaniu się jak najdalej od bieżącego węzła, powrocie i analizowaniu kolejnychmożliwości.
AISD str. TTTT
98
Rekursywny algorytm DFS ma następującą postać:// Wska•niki wizytacji maj• warto•• FALSE dla wszystkich
// w•z•ów w grafie
Depth_First_Search (VERTEX A)
{
Visit A;
Set_ON_Visit_Flag (A); // to TRUE
For all adjecent vertices Ai (i = 1, ..., p) of A
if (Ai has not been previously visited)
Depth_First_Search (Ai);
}
Przyk•ad.
W•ze• S•siedzi
A B C D A B, H
B A, C, G, F, H
C B, D, F
H G F E D C, E, F
E D, F
F B, C, D, E, G
G B, F, H
H A, B, G
Przeglądanie wszerz (BFS) polega na przeglądaniu danego węzła i wszystkich jego sąsiadów, apotem po kolei sąsiadów sąsiadów, itd.
Algorytm BFS1. Odwiedź węzeł startowy A.2. Wstaw węzeł A do kolejki.3. Dopóki kolejka nie jest pusta wykonuj kroki 3.1 - 3.2
3.1. Weź węzeł X z kolejki3.2. Dla wszystkich sąsiadów Xi (X-a) wykonuj
3.2.1 Jeśli Xi, był odwiedzony zakończ jego przetwarzanie.3.2.2 Odwiedź węzeł Xi.3.2.2 Wstaw do kolejki węzeł Xi.
Definicja. Graf w ujęciu ATD jest strukturą danych, która zawiera skończony zbiór węzłów iskończony zbiór krawędzi oraz operacje pozwalające na definiowanie, manipulowanie orazabstrahowanie pojęć węzłów i krawędzi.
Przykładowe metody klasy reprezentującej graf
Twórz i inicjalizuj obiekt grafu.Usuń obiekt grafu.Sprawdź, czy zbiór węzłów jest pusty.Sprawdź, czy zbiór krawędzi jest pusty.
AISD str. UUUU
99
Sprawdź, czy dwa węzły są sąsiednie.Buduj graf z danego zbioru węzłów i krawędzi.Dodaj węzeł do grafu.Szukaj węzła przechowującego zadany klucz.Usuń węzeł przechowujący zadany klucz.Modyfikuj informację w węźle o zadanym kluczu.Dodaj krawędź do grafu.Szukaj krawędź identyfikowaną przez zadany klucz.Usuń krawędź identyfikowaną przez zadany klucz.Przejdź graf metodą w głąb.Przejdź graf metodą wszerz.Drukuj obiekt grafu.Określ atrybuty grafu.Znajdź (oblicz) najkrótszą ścieżkę w grafie.
Implementacja grafu przy pomocy macierzy powiązań (sąsiedztwa).
Dla grafu bez wagi1, jeśli Ai jest sąsiednie z Aj
Adj_Mat[i][j] =0, jeśli Ai nie jest sąsiednie z Aj
Dla grafu z wagamiwij, jeśli Ai jest sąsiednie z Aj
Adj_Mat[i][j] =0, jeśli Ai nie jest sąsiednie z Aj
Dla grafu nieskierowango macierz powiązań jest symetrycznaAdj_Mat[i][j] = Adj_Mat[j][i]Do oznaczenia braku sąsiedztwa zamiast 0 stosuje się czasami oznaczenie nieskończoności.
Algorytm Dijkstry wyznaczania najkrótszej ścieżki w grafie.1. Dany jest graf ważony G ze zbiorem n węzłów:
A = A0, A1, A2, ... , An-1 = Q,krawędzi (Ai, Aj) (lub <Ai, Aj>) i wag wij
2. Zbiór roboczy węzłów S jest początkowo pusty. Każdemu węzłowi jestprzypisywana etykieta L, która ma następujące znaczenie: L(Ak) oznacza wagęnajkrótszej ścieżki od węzła początkowego A do węzła Ak. L(A)=0 i dla wszystkichwęzłów L(Ai)=nieskończoność (np. 99999999)
3. (Iteracja) Dopóki węzeł docelowy Q nie jest w zbiorze S wykonuj kroki 3.1 - 3.5. 3.1. Wybierz najbliższy węzeł Ak, taki że nie należy do S i L(Ak) jest najmniejsze
spośród wszystkich węzłów nie należących do S. 3.2. Dodaj Ak do zbioru S 3.3. Jeśli Ak=Q, zwróć L(Ak) i zakończ
AISD str. VVVV
100
3.4. Dla wszystkich sąsiadów Aj węzła Ak, które nie są w S modyfikuj ich etykietę Lprzez minimum z L(Aj) i z L(Ak)+wkj
3.5. Wróć do początku kroku 3.4. L(Q) jest wagą najkrótszej ścieżki od A do Q. Droga taka została znaleziona.