Laboratorium 3 Drzewa, kopce, wyszukiwanie wzorca informacji, kodowanie 1. Cel ćwiczenia Celem ćwiczenia jest praktyczne zapoznanie się z wybranymi algorytmami operującymi na strukturach drzewiastych dla różnych typów danych, złożonością obliczeniową, wydajnością i możliwością jej poprawy w wybranych przypadkach. 2. Wstęp W trakcie ćwiczenia badane są właściwości drzewiastych struktur danych i operujących na nich algorytmów. Pod pojęciem drzewiastej struktury danych jest tu rozumiany sposób organizacji danych pod względem sposobu ich przechowywania, organizacji i dostępu. Program analizowany i modyfikowany w trakcie laboratorium składa się z następujących plików składowych: plik aisd.h zawiera: deklarację i definicję typu wyliczeniowego Except wykorzystywanego do obsługi wyjątków rzucanych w programie, zawiera deklarację i definicję typu strukturalnego Parametry wykorzystywanego do analiz zaimplementowanych algorytmów zawiera deklarację i definicję szablonu głównej klasy AISD. plik dane.h zawiera: deklarację i definicje klasy nodet implementującej węzły drzewa deklarację i definicje klasy STOS implementującej podstawową funkcjonalność stosu deklarację i definicje klasy KOLEJKA implementującej podstawową funkcjonalność kolejki plik data_bi.h zawiera: deklarację i definicje klasy BiData implementującej przykładowy dwuskładnikowy typ danych przeciążenia wybranych operatorów współpracujących z obiektami klasy BiData plik data_huf.h zawiera: deklarację i definicje klasy HufData implementującej przykładowy typ danych dla analizy algorytmów wykorzystujących drzewo Huffmana przeciążenia wybranych operatorów współpracujących z obiektami klasy HufData plik drzewa.h zawiera: zawiera deklarację i definicję szablonu klasy Drzewa i metod operujących na przykładowych strukturach danych plik drzewa_bi.h zawiera: zawiera deklarację i definicję szkieletu (nie szablonu!) klasy DrzewaBi utworzonej na bazie klasy szablonowej Drzewa wraz z metodami operującymi na przykładowych strukturach danych typu BiData. Klasa DrzewaBi przeznaczona jest do w pełni samodzielnej implementacji wybranych funkcjonalności dla algorytmów operujących na drzewach. plik drzewa_huffman.h zawiera: zawiera deklarację i definicję klasy DrzewaHuf utworzonej na bazie klasy szablonowej Drzewa wraz z metodami operującymi na przykładowych strukturach danych typu HufData plik drzewa_int.h zawiera: zawiera deklarację i definicję klasy DrzewaInt utworzonej na bazie klasy szablonowej Drzewa wraz z metodami operującymi na przykładowych strukturach danych typu int plik drzewa_fix.h zawiera: zawiera deklarację i definicję klasy Fix utworzonej bezpośrednio na bazie klasy szablonowej AISDE wraz z metodami operującymi na przykładowych strukturach danych typu char. plik aisd.cpp zawiera: procedurę obsługi wyjątków plik main.cpp zawiera: główną pętlę programu. 1/7
Wszystkie algorytmy zostały zaimplementowane w postaci metod klasy Drzewa (plik drzewa.h, wydruk R.1), która podobnie jak wykorzystywana w poprzednim ćwiczeniu klasa AlgorytmySortowania, jest szablonem klasy pochodnej szablonu klasy wirtualnej AISD. Zdefiniowanie klas AISD i Drzewa w postaci szablonów daje możliwość ich zastosowania dla wielu różnych typów danych określonych parametrem (tu OBJ). Mogą to być podstawowe typy i struktury danych lecz również dowolne typy użytkownika. W każdym przypadku niezbędne odpowiednie dla konkretnego typu danych przeciążenie wykorzystywanych operatorów jak np. >, >=, <, <=, =, ==, ++, --, +=, -= itd. R.1. template <class OBJ> class Drzewa:public AISD<OBJ>{ inline int NodeL(int i){return 2*i; inline int NodeR(int i){return 2*i+1; inline int Parent(int i){return i/2; void FixUp(int); void FixDown(int,int); void InOrderRekur(int Node); //zwiedzanie rekurencyjne "in-order" void PreOrderRekur(int Node); //zwiedzanie rekurencyjne "pre-order" void PostOrderRekur(int Node); //zwiedzanie rekurencyjne "post-order" void PreOrderNRekurSTOS(int Node); //zwiedzanie nierekurencyjne ze stosem void HorizontNRekurKOLEJKA(int Node); //zwiedzanie nierekurencyjne z kolejka Parametry* SortKopcowanie (int L=1,int R=1); //sortowanie przez kopcowanie nodet<obj>* TurniejMax(int,int); //turniejowe poszukiwanie najwiekszego elementu w tablicy void TurniejSort(int,int); //turniejowe sortowanie w tablicy void Turniej(int,int); //rekurencyjny turniej w tablicy OBJ virtual *Visit(int){return 0; Drzewa(int LiczZbior):AISD<OBJ>(LiczZbior){ ~Drzewa(){ void VisitInOrder(int Node); void VisitPreOrder(int Node); void VisitPostOrder(int Node); void VisitPreOrderSTOS(int Node); void VisitHorizontKOLEJKA(int Node); void TurniejMax(); void TurniejSort(); void Kopcowanie(); void virtual WypiszDrzewoNaEkran(bool) {printf("niezdefiniowana funkcja wirtualna\n ); void virtual ZapiszDrzewoDoPliku(char*) {printf("niezdefiniowana funkcja wirtualna\n ); drzewa.h Przedstawiona na wydruku R.2 klasa AISD implementuje niezbędne metody operujące na strukturze danych, służące do uruchamiania testowanych algorytmów, wydruku raportów, zapisu danych na dysk, komunikacji z użytkownikiem itd. template <class OBJ> class AISD{ OBJ *Dane; OBJ *DaneKopia; int LicznoscZbioru; Parametry ZA; //ZlozonoscAlgorytmu - Parametry void OdswiezDane(); // przepisuje Dane_kopia do Dane void ZA_Init(); // Inicjalizacja struktury danych ZA long PobierzCzas(); inline void Zamien(OBJ &A,OBJ &B); // zamienia wartosci argumentow A i B inline void ZamienWarunkowo(OBJ &A,OBJ &B); // zamienia wartosci argumentow gdy B<A AISD(int); ~AISD(); OBJ *GetDane(); OBJ *GetDaneKopia(); int GetLicznosc(); void Raport(char*); void virtual ZapiszStanDoPliku(char*) void virtual WczytajDaneZPliku(char*) void virtual WypiszStanNaEkran(bool) void virtual BadanieAlgorytmow() {printf("niezdefiniowana funkcja wirtualna\n ); {printf("niezdefiniowana funkcja wirtualna\n ); {printf("niezdefiniowana funkcja wirtualna\n ); {printf("niezdefiniowana funkcja wirtualna\n ); R.2. aisd.h Klasa szablonowa Drzewa składa się z części publicznej (public) i chronionej (protected). Składniki i metody umieszczone w publicznej części są bezpośrednio dostępne dla użytkownika. W ten sposób zaimplementowane zostały: trzy algorytmy rekurencyjnego przechodzenia przez drzewo w porządkach: in-order, pre-order i postorder, dwie nierekurencyjne wersje algorytmu przechodzenia przez drzewo w porządku pre-order wykorzystujące stos i kolejkę, algorytm poszukiwania największego elementu tablicy metodą turnieju, algorytm sortowania przechowujący dane w strukturze kopca algorytm tworzenia drzewa Huffmana wraz z metodami niezbędnymi do analizy przykładowych danych W chronionej części klasy szablonowej Drzewa zadeklarowano metody NodeL, NodeR, Parent, FixUp, FixDown, Visit. Są to metody pomocnicze dla algorytmów sortujących. W celu zapewnienia integralności, wywołanie powyższych metod jest możliwe wyłącznie z wnętrza obiektów tej samej klasy lub klas od niej pochodnych (klauzula protected). Dla ułatwienia analizy złożoności zaimplementowanych algorytmów 2/7
wszystkie metody operujące na drzewach na bieżąco aktualizują zawartość struktury ZlozAlg o postaci przedstawionej na listingu R.3. R.3. typedef struct{ unsigned long CzWyk; // czas wykonania algorytmu unsigned long GlStoPr; // biezaca glebokosc stosu aplikacji unsigned long GlStoPrMax; // maksymalna glebokosc stosu aplikacji unsigned long GlStoDa; // biezaca glebokosc stosu danych unsigned long GlStoDaMax; // maksymalna glebokosc stosu danych unsigned long LiKop; // liczba kopiowan danych (3 kopiowania na 1 zamiane) unsigned long LiPor; // liczba porownan danych unsigned long LiWej; // liczba wejsc do komorki unsigned long LiPut; // liczba wstawien do kolejki unsigned long LiPush; // liczba odlozen na stos } Parametry; aisd.h Każda z zaimplementowanych metod operuje na danych umieszczonych w tablicy wskazywanej przez Dane prywatny składnik szablonu klasy inicjalizowany w czasie pracy konstruktora szablonu klasy macierzystej AISDE. Liczność zbioru jest w określona wartością zmiennej chronionej LicznośćZbioru inicjalizowanej przy tworzeniu obiektu (w przykładowej implementacji jest wykorzystywana globalna zmienna o tej samej nazwie LicznośćZbioru). W części publicznej klasy Drzewa zadeklarowano konstruktor, destruktor oraz wirtualne metody umożliwiające wypisanie danych na ekran i operacje na plikach (WypiszStanNaEkran, WypiszDrzewoNaEkran, ZapiszStanDoPliku, ZapiszDrzewoDoPliku i WczytajDaneZPliku). Wywołanie metod wirtualnych WypiszDrzewoNaEkran i WypiszStanNaEkran wymaga podania parametru logicznego Ekran. Steruje on wypisywaniem zbioru danych na ekran. Wywołanie metod ZapiszStanDoPliku, ZapiszDrzewoDoPliku i WczytajDaneZPlikuwymaga podania nazwy pliku na którym metody te mają pracować. Klasy AISD i Drzewa to szablony klas wirtualnych. Aby można było utworzyć obiekty tego typu niezbędne jest zdefiniowanie stosowych klas pochodnych jak również zdefiniowanie metod wirtualnych WypiszStanNaEkran, ZapiszStanDoPliku, WczytajDaneZPliku i BadanieAlgorytmów. W analizowanym w trakcie ćwiczenia programie dla przykładu zdefiniowano złożone typy BiData i HufData. Są to klasy dla których odpowiednio przeciążone zostały wybrane operatory (pliki data_bi.h i data_huf.h wydruki R.4 i R.5). R.4. class BiData{ int i; // pole danych: klucz 1 char str[16]; // pole danych: klucz 2 BiData(); // konstruktor BiData(int i,char *str); // konstruktor void Set(int i,char *str); int GetI(); char *GetStr(); bool operator< (BiData Dane); // przeciazony operator porownania bool operator> (BiData Dane); // przeciazony operator porownania bool operator<=(bidata Dane); // przeciazony operator porownania bool operator>=(bidata Dane); // przeciazony operator porownania BiData operator =(BiData Dane); // przeciazony operator przypisania int operator =(int Dane); // przeciazony operator przypisania char* operator =(char *Dane); // przeciazony operator przypisania data_bi.h W ćwiczeniu dotyczącym badania algorytmów sortowania złożony typ danych BiData wykorzystywany był do analizy stabilności algorytmów. Obecnie jego implementacja jest pozostawiona inwencji Studentów. Typ HufData jest przeznaczony do pracy z drzewami Huffmana. R.5. class HufData{ friend class KlasaDrzewaHUF; unsigned long Count; // liczba wystapien znaku o danym kodzie unsigned char Byte; // kod znaku HufData(); HufData(unsigned char i); void Set(unsigned long Count); void Set(unsigned char Byte); void Set(unsigned long Count, unsigned char Byte); unsigned long GetCount(); unsigned char GetByte(); bool operator< (HufData Dane); // przeciazony operator porownania < bool operator> (HufData Dane); // przeciazony operator porownania > bool operator<=(hufdata Dane); // przeciazony operator porownania <= bool operator>=(hufdata Dane); // przeciazony operator porownania >= bool operator!=(hufdata Dane); // przeciazony operator porownania!= bool operator==(hufdata Dane); // przeciazony operator porownania == HufData operator =(HufData Dane); // przeciazony operator przypisania = HufData operator++(); // przeciazony operator postinkrementacji ++ HufData operator++(int); // przeciazony operator preinkrementacji ++ HufData operator--(); // przeciazony operator postinkrementacji ++ HufData operator--(int); // przeciazony operator preinkrementacji ++ int operator =(int Dane); // przeciazony operator przypisania char* operator =(char *Dane); // przeciazony operator przypisania = data_huf.h Dla wybranego typu danych do analizy określonego parametrem OBJ szablonu klas AISD lub Drzewa niezbędne jest zdefiniowanie klasy pochodnej, z własnymi wersjami metod wirtualnych WypiszStanNaEkran, ZapiszStanDoPliku, WczytajDaneZPliku i BadanieAlgorytmów. W programie wykorzystywanym w czasie laboratorium do stworzenia klasy KlasaDrzewaINT wykorzystano podstawowy typ danych int (plik drzewa_int.h wydruk R.6). Klasa ta organizuje dane w strukturę drzewa. Same dane do sortowania mają postać tablicy 3/7
rekordów typu int, wskazywanej przez Dane (prywatny element klasy szablonowej Drzewa dziedziczony z klasy AISD). R.6. class KlasaDrzewaINT:public Drzewa<int>{ int *Visit(int); KlasaDrzewaINT(int LiczZbior):Drzewa<int>(LiczZbior){ ~KlasaDrzewaINT(){ void ZapiszStanDoPliku(char*); void WczytajDaneZPliku(char*); void WypiszStanNaEkran(bool); void WypiszDrzewoNaEkran(bool); void ZapiszDrzewoDoPliku(char*); void BadanieAlgorytmow(); //f. wirtualna - konieczna definicja drzewa_int.h W trakcie ćwiczenia badane będą również algorytmy analizy składniowej wyrażeń. Algorytmy tego typu są stosowane m.in. w kompilatorach programów, w których wykorzystywane są do konwersji zapisu kodu programu z poziomu kodu języka wysokiego poziomu na kod maszynowy. Z procesem tym bezpośrednio związane jest pojęcie notacji wyrażeń. Wyrażenia te rozumiane jako część kodu zapisanego w języku wysokiego poziomu mogą być zapisane w postaci infiksowej, postfixowej i prefiksowej. Dla zilustrowania zasady analizy zapisów notacji w ramach ćwiczenia analizowane będą wyrażenia algebraiczne. Zapis tego samego wyrażenia w różnych notacjach przestawia poniższa tabela. infix (5*(((9+8)*(4*6))+7)) postfix 598+46**7+* prefix *+7**46+895 Postać infiksowa jest zdecydowanie najbardziej rozpowszechniona. W notacji postfixowej operator znajduje się zawsze bezpośrednio po argumentach których dotyczy. Notacja ta nie wykorzystuje nawiasów. Przykładowe wyrażenie przedstawione w powyższej tabeli można uzyskać z postaci infiksowej na drodze następujących przekształceń: (5*(((9+8)*(4*6))+7)) 5(((9+8)*(4*6))+7)* 5((9+8)*(4*6))7+* 5(9+8)(4*6)*7+* 598+46**7+* Postać prefixowa to notacja odwrotna do postfixowej. Jest tworzona w sposób analogiczny do przekształcenia infix postfix. W celach treningowych w ramach przygotowania do ćwiczenia zalecane jest samodzielne wykonanie przekształceń infix postfix, infix prefix. Przykładowy analizator składniowy zaimplementowany w ćwiczeniu działa sekwencyjnie. Najpierw jest przeprowadzana zamiana postaci wyrażenia infixowej na postać postfixową. Następnie dane zostają zapisane w drzewiastej strukturze danych, po czym obliczana jest poszukiwana wartość analizowanego wyrażenia. Przykładową implementację algorytmu analizy składniowej opisuje metoda LiczPreFix klasy Fix pochodnej klasy szablonowej AISD (plik drzewa_fix.h wydruk R.7). Metoda LiczPreFix oblicza wartość wyrażenia podanego jako łańcuch znaków wskazywany przez Dane. Jest to metoda rekurencyjna, która wartość liczbową wyrażenia oblicza do momentu osiągnięcia końca tego wyrażenia. Dla zapewnienia składniowej poprawności opisu klasy Fix, zdefiniowano niezbędne ciała metod wirtualnych klasy bazowej AISD. class Fix:public AISD<char>{ Fix(int LiczZbior):AISD<char>(LiczZbior){ ~Fix(){ void ZapiszStanDoPliku(char*); void WczytajDaneZPliku(char*); void WypiszStanNaEkran(bool); void InFix2PostFix(); double LiczPreFix(); void BadanieAlgorytmow(); R.7. drzewa_fix.h 3. Przebieg ćwiczenia Funkcja main (plik main.cpp,r.8) zawiera następujące parametry zadeklarowane w programie jako zmienne globalne: Ekran zarządza wypisywaniem danych na ekran), Srand ustawia warunki początkowe dla generatora liczb pseudolosowych (w trakcie ćwiczenia każdy student przypisuje tej zmiennej swój numer albumu), Licznosc ustawia liczność zbioru do sortowania. int main(){ Ekran=true; Srand=10; Licznosc=25; try{ //tu wpisac numer indeksu 4/7
InitINT(); DrzewaINT->BadanieAlgorytmow(); InitHUF(); DrzewaHUF->BadanieAlgorytmow(); InitFIX(); DrzewaFIX->BadanieAlgorytmow(); // InitBI(); DrzewaBI->BadanieAlgorytmow(); } catch (Except e){ Error(e); } getchar(); CleanUp(); return 1; R.8. main.h W takcie ćwiczenia dla wskazanych przez prowadzącego algorytmów w miarę potrzeby należy przystosować kod programu do zaplanowanych pomiarów i symulacji. Przykładowe wywołania poszczególnych, metod badanych w trakcie ćwiczenia, zostały umieszczone wewnątrz metod wirtualnych BadanieAlgorytmów definiowanych dla każdego typu danych (OBJ) z osobna. Zależnie od wskazań prowadzącego należy: 1. przeanalizować sposób implementacji nierekurencyjnych metod przechodzenia przez drzewo w porządku pre-order. 2. zaproponować, zaimplementować i przetestować własne wersje nierekurencyjnych metod przechodzenia przez drzewo w porządku in-order. 3. zaproponować, zaimplementować i przetestować własne wersje nierekurencyjnych metod przechodzenia przez drzewo w porządku post-order. 4. przeprowadzić analizę zapotrzebowania na zasoby dla poszczególnych metod przechodzenia przez drzewo. (przykładowych i samodzielnie zaimplementowanych). 5. przeprowadzić analizę parametrów metody turniejowego poszukiwania największego elementu (algorytm, zajętość zasobów, złożoność czasowa). 6. przeprowadzić analizę wydajności działania metod sortowania przez kopcowanie. Wskazać ograniczenia i potencjalne możliwości doskonalenia przykładowych implementacji, 7. rozszerzyć funkcjonalność kodu o możliwość pracy algorytmów analizowanych podczas ćwiczeń na złożonych typach danych. Wykorzystać zaproponowany szkielet klasy BiData. Przetestować zaproponowane implementacje. 8. przeprowadzić analizę algorytmu sortowania wykorzystującego kopiec. Przetestować dla danych złożonych typu BiData. 9. przeprowadzić analizę działania metod służących budowie drzewa Huffmana. Wskazać ograniczenia przykładowej implementacji i potencjalne metody ulepszenia kodu. Na podstawie wpisów w strukturze ZA (klasa Parametry) określić aktualną złożoność poszczególnych algorytmów. Zweryfikować poprawność wpisów w strukturze ZA. 10. Określić przewidywany stopień kompresji możliwy do uzyskania przy wykorzystaniu drzewa Huffmana tworzonego w klasie DrzewaHuff dla podanego fragmentu tekstu Shakesperare a 11. przeprowadzić analizę działania i wydajności działania algorytmów analizy składniowej (klasa Fix) i dopisać opcję konwersji postaci infixowej na postfixową (implementacja metody InFix2PostFix klasy Fix) 12. rozszerzyć funkcjonalność metody LiczPreFix o działania - i /. 13. zaimplementować metody HuffmanPack i HuffmanUnpack i przetestować ich działania na podanym fragmencie tekstu Shakespeare a. 4. Analiza wyników, wnioski, sprawozdanie Na ocenę z ćwiczenia bezpośrednio wpływają: zawartość notatnika z ćwiczenia (w wersji papierowej) Na początku ćwiczenia należy wydrukować umieszczony na końcu niniejszej instrukcji Notatnik. Będzie on stanowił bieżący zapis przebiegu ćwiczenia i wyników pracy. zawartość oddanego w terminie sprawozdania (wyłącznie w postaci elektronicznej w formacie pdf). Sprawozdanie powinno zawierać następujące elementy: cel ćwiczenia (własnymi słowami), opis modyfikacji wprowadzonych do programu przykładowego wraz z analizą celowości tych zmian, stosowne wykresy (np. wykresy czasów wykonania programu w zależności od wersji badanego algorytmu w funkcji długości sortowanych danych, wykresy dynamiczne sortowania, czyli postęp posortowania danych w zależności od liczby wykonanych kroków algorytmu sortującego ) podsumowanie ćwiczenia czyli wnioski: konkretne, na temat, samodzielne, twórcze. Na końcową ocenę za ćwiczenie składają się sposób wykonania ćwiczenia, samodzielność i pomysłowość w jego wykonaniu, merytoryczna zawartość i forma i terminowość nadesłania sprawozdania. 5/7
Sprawozdanie powinno zostać nadesłane pocztą elektroniczną, w terminie podanym przez prowadzącego zajęcia (zazwyczaj 7 dni). Pod rygorem nie sprawdzania sprawozdania dodatkowo niezbędne jest wypełnienie przez studentów następujących wymogów formalnych: jedyny przyjmowany format sprawozdania to PDF, nazwa pliku zawierającego sprawozdanie musi spełniać wymogi wzorca AISDE_3_Nazwisko_Imie.pdf. temat maila ze sprawozdaniem musi pasować do wzorca: AISDE Nazwisko_Imie, sprawozdania oddawane przez studentów muszą spełniać wymogi określone przez wykładowcę na początku semestru. Uwagi W przypadku wykrycia nieprawidłowości w działaniu któregoś z algorytmów szczególnie premiowane będzie samodzielne rozwiązanie zauważonego problemu. Jednocześnie brak reakcji na niepoprawne wyniki w protokole będzie punktowany ujemnie 5. Literatura 1. R. Sedgevick Algorytmy w C++, Wydawnictwo RM, 1999, 2. J. Grębosz Symfonia C++ Wydawnictwo Oficyna Kallimach 1997 3. J. Grębosz Pasja C++ Wydawnictwo Oficyna Kallimach 1997 Opracowanie dr inż. Grzegorz Janczyk janczyk@imio.pw.edu.pl Weryfikacja dr inż. Adam Wojtasik aw@imio.pw.edu.pl 6/7
AISDE LAB 3 Imię i nazwisko, Grupa dziekaoska NOTATNIK Nr Indeksu Data wykonania ćwiczenia Data oddania protokołu Punkty do wykonania 1 2 3 4 5 6 7 8 9 10 11 12 13 Analizowany algorytm Parametry Uwagi / Zauważone błędy NOTATNIK NALEŻY ODDAĆ PROWADZĄCEMU ZAJĘCIA W CHWILI ZAKOŃCZENIA ĆWICZENIA