PODSTAWY INFORMATYKI wykład 4. Adrian Horzyk Web: http://home.agh.edu.pl/~horzyk/ E-mail: horzyk@agh.edu.pl Google: Adrian Horzyk Gabinet: paw. D13 p. 325 Akademia Górniczo-Hutnicza w Krakowie WEAIiE, Katedra Automatyki http://www.agh.edu.pl Mickiewicza Av. 30, 30-059059 Cracow, Poland
Struktury danych Przy rozwiązywaniu problemu (za pomocą komputera, czy też bez niego) trzeba dokonać wyboru pewnego abstrakcyjnego modelu rzeczywistości, czyli zdefiniować zbiór danych mających reprezentować rzeczywistą sytuację. W tym celu wykorzystujemy pewne specyficzne zbiory danych, które charakteryzują się różną strukturą wewnętrzną ułożenia tych danych oraz różnym sposobem zapisu i dostępu do tych danych. Dostęp do odpowiednich składowych takiego zbioru może w dużym stopniu wpłynąć na złożoność obliczeniową całego algorytmu więc wybór odpowiednich struktur danych jest sprawą kluczową decydującą o efektywności działania programów. Wybór odpowiednich struktur danych podyktowany jest poprzez rozwiązywany problem oraz możliwości obliczeniowe komputera. Wybór reprezentacji danych zależny jest nie tylko od możliwości komputera, lecz również zależny jest od przewidywanych operacji, jakie na danych będą wykonywane. Wykład 4. Strona 2.
Rodzaje podstawowych struktur danych Tablica składa się z wielu elementów tego samego typu ułożonych jedna po drugiej z możliwością odwoływania się do nich za pomocą indeksów. Rekord składa się z wielu elementów różnego typu, do których odwołujemy się za pomocą ich nazwy. Zbiór składa się z wielu elementów tego samego typu. Plik sekwencyjny składa się z sekwencji elementów różnego typu. Ciąg znaków składa się z sekwencji elementów typu znakowego. Kolejki FIFO (first in first out) składa się z ciągu elementów tego samego typu z możliwością wkładania elementów na jej końcu i wyciągania elementów z jej początku. Stos FILO (first in last out) składa się z ciągu elementów tego samego typu z możliwością wkładania i wyciągania elementów tylko z jego końca. Lista (pojedynczo lub podwójnie wiązane) składa się z elementów tego samego typu tak, że elementy są wzajemnie ze sobą powiązane tak, że każdy element zna tylko swojego następnika (ew. również poprzednika). Drzewo (binarne, rozpinające, pozycyjne, turniejowe) składa się z elementów tego samego typu ułożonych hierarchicznie w taki sposób, że każdy węzeł drzewa może być rodzicem dla innych węzłów, a węzły mające swojego rodzica nazywamy dziećmi. Graf (skierowane i nieskierowane, cykliczne, acykliczne, planarne) składa się z wierzchołków i krawędzi, które łączą ze sobą poszczególne wierzchołki w różny sposób. Wykład 4. Strona 3.
Tablica Tablica jest strukturą jednorodną, która składa się z elementów tego samego typu; charakteryzuje się swobodnym dostępem do danych, zaś składowe tablicy są indeksowane, co umożliwia łatwy i szybki dostęp do poszczególnych składowych tablicy: Definiujemy tablicy w Pascalu: type T = array [ZAKRES] of TYP; Deklaracja tablicy w Pascalu: var x: Wektor; np. type Wektor = array [1..50] of real; var i: Integer; Odwołanie do elementów tablicy poprzez odpowiedni indeks i, np.: x[i] Indeks tablicy może być stałą (np. x[3]), zmienną (np. x[i]), jak również wyrażeniem indeksowym (np. x[2*i+1]). Przykład 1. Poszukiwanie najmniejszego indeksu i składowej o wartości x var A: array[1.. ] of T { >0}; i:=0; repeat i:=i+1 until (A[i]=x) or (i= ); var i : Integer; if A[i] <> x then Writeln( ie ma takiego elementu w tablicy A. ) else Writeln ( Znaleziono x na, i, -tej pozycji w tablicy A. ); Wykład 4. Strona 4.
Tablica uporządkowana i wielowymiarowa Jeżeli elementy tablicy zostały wcześniej uporządkowane/posortowane przeszukiwanie można znacznie przyspieszyć. W takim przypadku stosuje się metodę połowienia przedziału, w którym może się znajdować poszukiwany element. Metodę tą nazywamy metodą bisekcją lub przeszukiwaniem połówkowym: i:=1; j:= ; repeat k:=(i+j) div 2 if x>a[k] then i:=k+1 else j:=k-1 until (A[k]=x) or (i>j) Górną granicą wymaganej liczby porównań jest log 2 Macierz (tablica dwuwymiarowa) to tablica, której składowe są również tablicami: np. M: array [1..10] of Wektor jest tablicą o 10 składowych (wierszach), z których każdy wiersz składa się z 50 składowych typu real i nazywa się macierzą 10x50 o składowych rzeczywistych lub też macierzą o 10 wierszach i 50 kolumnach. Odwołujemy się do elementów macierzy poprzez odpowiednie indeksy: M[i][j] lub M[i,j] Deklarację macierzy można uprościć w następujący sposób w Pascalu: M: array [1..10] of array [1..50] of real; lub M: array [1..10,1..50] of real; Wykład 4. Strona 5.
Record Rekord jest najprostszym sposobem tworzenia typów złożonych, składających się z elementów dowolnego typu. Definicja typu record w Pascalu: type LiczbaZespolona = record re, im: real; end type Osoba = record imie, nazwisko: alfa; dataurodzenia: Data; plec: (kobieta, mezczyzna); stancywilny: (wolny, zonaty, owdowialy, rozwiedziony) end Deklaracja zmiennej typu rekord w Pascalu: z: LiczbaZespolona; p: Osoba; Konstruujemy/inicjujemy rekord przez: z = LiczbaZespolona (1.0, -2.0); p = Osoba ( Jan, owak, Data(19,3,1976), mezczyzna, wolny); Wykład 4. Strona 6.
Zbiór Zbiór odnosi się do elementów tego samego typu. Definiujemy go następująco: type Z = set of TYP np. type zbiorznakow = set of char Deklaracja zbioru: zz := [ +, -, d, g, 5 ] a wszystkich typach zbiorowych są określone następujące operatory elementarne, uszeregowane według priorytetów: * - przecięcie (mnożenie) zbiorów + - suma (dodawanie) zbiorów - - różnica zbiorów in - należenie do zbioru r*s+t = (r*s) + t; r-s*t = r-(s*t); r-s+t = (r-s)+t; x in s+t = x in (s+t) Zbiór S wygodnie jest reprezentować w pamięci komputera za pośrednictwem jego funkcji charakterystycznej C(s). Jest to wektor wartości logicznych, którego i-ta składowa określa występowanie bądź brak wartości i w zbiorze, np. zbiór liczb całkowitych: S = [1,4,8,9] jest reprezentowany przez ciąg wartości logicznych F (fałsz) i T (prawda) następująco: C(s) = (FTFFTFFFTT) jeśli typem podstawowym zbioru S jest 0..9. Wykład 4. Strona 7.
Zbiór i jego reprezentacja w pamięci W pamięci komputera sekwencja wartości logicznych jest reprezentowana przez ciąg bitów: S: 0 1 0 0 1 0 0 0 1 1 - reprezentacja w pamięci 0 1 2 3 4 5 6 7 8 9 - kolejne bity Reprezentacja zbioru przez jego funkcję charakterystyczną ma tę zaletę, że operacje obliczania sumy, przecięcia i różnicy dwóch zbiorów można realizować w maszynie cyfrowej jako elementarne operacje logiczne. Sprawdzanie czy element x należy do zbioru możemy wykonać poprzez badanie warunku: if x in [c 1, c 2,..., c n ] then Zamiast if (x= c 1 ) or (x= c 2 ) or or (x= c n ) then Wykład 4. Strona 8.
Plik sekwencyjny (ciąg) Plik sekwencyjny (ciąg) należy do struktur danych o mocy nieskończonej: type tekst = file of char {definicja typu pliku znakowego (tekstowego)} rewrite(x) {konstruowanie pliku/ciągu pustego} put(x) {wydłuża plik/ciąg x poprzez dołączenie na jego końcu elementu z bufora} reset(x) {zapoczątkowuje przeglądanie pliku/ciągu x} get(x) {przejście do następnej składowej, odczytanie jej i przypisanie do bufora} eof(x) {służy do badania, czy został osiągnięty koniec pliku x} read(x,v) {odczytanie elementu z ciągu x i zapisanie go do zmiennej v} write(x,v) {wydłuża plik/ciąg x poprzez dołączenie na jego końcu elementu v} Przykłady: rewrite (x); while p do begin Operacja(v); write (x,v) end; reset (x); while not(eof(x)) do begin read(x,v); Operacja(v) end; Wykład 4. Strona 9.
Teksty i pliki tekstowe Teksty odgrywają szczególnie istotną rolę w przetwarzaniu danych, albowiem służą do komunikacji użytkownika z komputerem. type tekst = file of char; var input, output: tekst; writeln(f) {dołącz znak końca wiersza do pliku f} readln(f) {przeskocz ciąg znaków pliku f aż do znaku występującego bezpośrednio po najbliższym znaczniku końca wiersza} eoln(f) {funkcja booloa, która przyjmuje wartość true, jeśli aktualna pozycja pliku azuje na separator wiersza, w przeciwnym przypadku przyjmuje wartośćfalse} rewrite(f); while not(q) do begin while not(p) do begin Operacja(x); write (f,x) end; writeln(f) end reset(f); while not eof(f) do begin Operacja1(); while not eoln(f) do begin read(f,x); Operacja2(x); end; Operacja3(); readln(f) end Wykład 4. Strona 10.
Dynamiczne struktury danych Dynamiczne struktury danych charakteryzują się zmiennością swoich struktur podczas procesu obliczeniowego. Zmiany te dotyczą przede wszystkim ich rozmiarów (ilości zajętej pamięci). a dynamiczne struktury danych kompilator nie może z góry przydzielić określonej ilości pamięci podczas dokonywanej translacji kodu, gdyż jej ilość może się zmieniać. Mówimy więc o tzw.dynamicznym przydzielaniu pamięci już podczas wykonywania programu. Oznacza to przydzielanie pamięci każdej ze składowych w chwili powołania danego obiektu do życia. Kompilator w procesie translacji przydziela więc tylko stałą ilość pamięci na zapamiętanie adresu składowej umieszczanej dynamicznie w pamięci zamiast samej składowej. Zmienna statyczna powiązana jest z konkretnym miejscem i rozmiarem pamięci już w momencie kompilacji programu. Zmienna dynamiczna jest tworzona i usuwana podczas wykonywania programu. Tak więc pamięć potrzebna do zapamiętania zmiennej dynamicznej może być przydzielana (w miarę dostępnych zasobów), zwalniana i znowu przydzielana innej zmiennej. W Pascalu można tworzyć proste zmienne dynamiczne oraz dynamiczne struktury danych, np. listy. Dynamiczne struktury danych mogą się rozszerzać lub kurczyć w miarę potrzeb w trakcie wykonywania programu. Wykład 4. Strona 11.
Wskaźniki i przykład ich zastosowania Dynamiczny przydział pamięci jest realizowany za pomocą aźników, które są specjalnym typem zmiennej, która nie przechowuje żadnych danych, lecz adres ich położenia w pamięci. W programie można zmienić wartość aźnika, tak aby azywał na inne miejsce pamięci lub żeby nie azywał żadnego miejsca ( IL). Można też zwolnić miejsce związane z określonym aźnikiem i przydzielić je innym zmiennym. Listy są przydatne w sytuacjach, gdy nie można przewidzieć zapotrzebowania na pamięć. Wskaźniki służą do powiązania poszczególnych elementów listy ze sobą. Każdy element listy zawiera minimalnie jeden aźnik, który azuje następny element listy (lista pojedynczo wiązana). Można też zastosować dwa aźniki, wtedy azywany jest również element poprzedni listy. Tablice można traktować jako przeciwieństwo list, gdyż te wymagają określenia wielkości w momencie ich tworzenia bez dalszej możliwości ich zwiększania czy zmniejszania. Wadą tablic jest więc konieczność przydzielania takiej ilości pamięci, ile wymaga maksymalna przewidziana liczba danych. Takie postępowanie może być dużym marnotrawstwem pamięci. Wykład 4. Strona 12.
Definiowanie aźników i operacje na aźnikach Wskaźniki mogą odwoływać się do danych dowolnego typu. W Pascalu chcąc odnieść się do obiektu azywanego przez aźnik dodajemy daszek ^. Deklaracja aźnika azującego na zmienną typu integer : var Wsk_Int : ^integer; astępnie trzeba przydzielić pamięć dla danej typu integer azywanej przez ten aźnik: new (Wsk_Int); Przypisanie: Wsk_Int^ := 500; Zmienna = Wsk_Int^; Działania na aźnikach ograniczają się do porównania, np.: if Wsk1 = Wsk2 then...; i do przypisania wartości, np.: Wsk1 := Wsk2; W wyniku takiego działania zmienia się miejsce odwołania aźnika Wsk1 na takie samo, jakie ma aźnik Wsk2. Miejsce pamięci azywane wcześniej prze Wsk1 jest po wykonaniu działania niedostępne (o ile nie azywał na nie inny aźnik). Zwalnianie miejsca pamięci azywanego przez aźnik: dispose (Wsk_Int); Wykład 4. Strona 13.
Funkcje i procedury na aźnikach ew tworzy nową dynamiczną zmienną i przypisuje jej adres zmiennej: procedure ew(var P: Pointer); Dispose zwalnia pamięć przydzieloną dynamicznej zmiennej: procedure Dispose(var P: Pointer); Przykłady: type Str18 = string[18]; var P: ^Str18; begin ew(p); P^ := ' ow you see it...'; Dispose(P); { ow you don't... } end. Wykład 4. Strona 14.
Dynamiczny przydział pamięci GetMem tworzy dynamiczną zmienną o podanej wielkości i przypisuje adres tego bloku do wyspecyfikowanego aźnika: procedure GetMem(var P: Pointer; Size: Word); FreeMem zwalnia pamięć zajmowaną przez dynamiczną zmienną o określonej wielkości: procedure FreeMem(var P: Pointer; Size: Word); Przykłady: type TFriendRec = record ame: string[30]; Age : Byte; end; var p: pointer; begin if MaxAvail < SizeOf(TFriendRec) then Writeln(' ot enough memory') else begin GetMem(p, SizeOf(TFriendRec)); { Allocate memory on heap } {...Use the memory... } FreeMem(p, SizeOf(TFriendRec)); { Then free it when done } end; end. Wykład 4. Strona 15.
Stos (FILO First In Last Out) STOS FILO DODAWA IE USUWA IE góra góra +1. góra. -1. -2.. -1. -2.. -1. -2. 1. IL 1. IL 1. IL Wykład 4. Strona 16.
Definicja i deklaracja stosu w Pascalu Deklaracja dynamicznej struktury stosu: TYPE StosPtr = ^Stos; Stos = record dolny : StosPtr; zawartosc: integer end Tworzenie pustego stosu: VAR wierzch : StosPtr; wierzch := IL; Wstawianie nowego elementu (recordu) na górę stosu: VAR nowy : StosPtr; ew (nowy); nowy^.dolny := wierzch; wierzch := nowy; Usuwanie górnego elementu (recordu) ze stosu: if (wierzch^.dolny) then begin usuwany := wierzch; wierzch := wierzch^.dolny; Dispose (usuwany) end else begin Dispose (wierzch); wierzch := IL end; Wykład 4. Strona 17.
Kolejki (FIFO First In First Out) Dynamiczna struktura kolejki: koniec. IL -1. -2. 1. początek Usuwanie elementu (rekordu) z początku kolejki: koniec. IL -1. 2. 1. początek Dodawanie nowego elementu (rekordu) na koniec kolejki: koniec. -1. -2. 1. początek +1. IL Wykład 4. Strona 18.
Definicja i deklaracja kolejki w Pascalu Deklaracja dynamicznej struktury kolejki: TYPE KolejkaPtr = ^Kolejka; Kolejka = record nastepny : KolejkaPtr ; zawartosc: integer end Pusta kolejka: VAR poczatek, koniec : KolejkaPtr ; poczatek := IL; koniec := IL; Wstawianie nowego elementu (recordu) na koniec kolejki: VAR nowy : KolejkaPtr ; ew (nowy); koniec^.nastepny := nowy; nowy^.nastepny := IL; koniec := nowy; Usuwanie elementu (recordu) z przodu kolejki: if (poczatek) then begin usuwany := poczatek; poczatek := poczatek^.nastepny; Dispose (usuwany) end Wykład 4. Strona 19.
Listy pojedynczo wiązane Przykład listy pojedynczo wiązanej: początek 1. 2. 3.. IL Wklejanie elementu (rekordu) na początek listy: OWY początek 1. 2. 3. Wklejanie elementu (rekordu) do środka listy: OWY. IL początek 1. 2. 3.. IL Wykład 4. Strona 20.
Listy pojedynczo wiązane Przykład listy pojedynczo wiązanej: początek 1. 2. 3.. IL Usuwanie elementu (rekordu) z początku listy: początek 1. 2. 3.. IL Usuwanie elementu (rekordu) ze środka listy: początek 1. 2. 3.. IL Wykład 4. Strona 21.
Listy podwójnie wiązane Przykład listy pojedynczo wiązanej: koniec początek 1. następny 2. następny 3. następny. IL IL poprzed. poprzed. poprzed. Wklejanie elementu (rekordu) do środka listy: OWY następny poprzed. koniec początek 1. następny 2. następny 3. następny. IL IL poprzed. poprzed. poprzed. Usuwanie elementu (rekordu) ze środka listy: koniec początek 1. następny IL 2. następny poprzed. 3. następny poprzed.. IL poprzed. Wykład 4. Strona 22.
Implementacja listy z wykorzystaniem tablicy INDEKSY TABLICY: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. NASTĘPNIKI ELEMENTÓW LISTY PODWÓJNIE WIĄZANEJ: 1. 2 2. 3 3. 6 8. 9 7. 4 4. 7 5. 8 6. 5 9. 10 10. 11 11. 0 0 1 2 5 8 3 6 7 4 9 10 POPRZEDNIKI ELEMENTÓW LISTY PODWÓJNIE WIĄZANEJ : Listę wobec tego możemy zaimplementować również z wykorzystaniem tablicy. Mankamentem takiej implementacji jest niemożliwość rozszerzenia listy poza wyznaczony rozmiar tablicy oraz ew. nieoszczędność takiej implementacji, jeśli lista jest mała, a zadeklarowania tablica bardzo duża. Wykład 8. Strona 23.
Definicja i deklaracja listy w Pascalu Definicja i deklaracja dynamicznej struktury listy podwójnie wiązanej: TYPE ListPtr = ^List; List = record nastepny, poprzedni : ListPtr; zawartosc: integer end Tworzenie pustej listy dwukierunkowej: VAR poczatek, koniec : ListPtr; poczatek := IL; koniec := IL; Wstawianie nowego elementu (recordu) po aktualnym aźniku do listy: VAR nowy : ListPtr; ew (nowy); nowy^.nastepny := aktualny^.nastepny; nowy^.poprzedni := aktualny; nowy^.nastepny^.poprzedni := nowy; aktualny^.nastepny := nowy; Usuwanie azywanego elementu (recordu) z listy: aktualny^.następny^.poprzedni := aktualny^.poprzedni; aktualny^.poprzedni^.nastepny := aktualny^.nastepny; if (aktualny^.poprzedni) begin aktualny := aktualny^.poprzedni; Dispose (aktualny^.nastepny) end else if (aktualny^.nastepny) begin aktualny := aktualny^.nastepny; Dispose (aktualny^.poprzedni) end else begin Dispose (aktualny); aktualny := IL end; Wykład 4. Strona 24.
Edycja kodu źródłowego w Pascalu PROGRAM Nazwa; USES NazwaBiblioteki; CONST NazwaStalej = WartoscStalej; TYPE NazwaTypu = DefinicjaTypu; VAR NazwaZmiennejGlobalnej : TypZmiennejGlobalnej; BEGIN Readln (a); {wielkość liter w nazwach instrukcji nie ma znaczenia} FOR i=1 TO a DO BEGIN IF i<10 THEN Writeln ( Mała liczba ) {brak średnika przed ELSE} ELSE Writeln ( Duża liczba ) {brak średnika przed END} END Writeln ( Wcięcia w programie powinny odwzorowywać strukturę! ); REPEAT UNTIL Keypressed {brak średnika przed END} END. {program główny w Pascalu kończy się kropką} {Wcięcia ułatwiają orientację w programie i jego czytelność.} Wykład 4. Strona 25.
Algorytm obliczania NWD - klasyczny Klasyczny szkolny algorytm charakteryzuje się dużą złożonością obliczeniową. Wykład 4. Strona 26.
Algorytm obliczania NWD poszukiwanie efektywnego alg. Dążenie do efektywności i optymalizacji to cecha dobrych informatyków! Wykład 4. Strona 27.
Wyszukiwanie z wykorzystaniem wartownika Zadanie wyszukania słowa w słowniku uporządkowanych słów. Wykład 4. Strona 28.
Wyszukiwanie połówkowe Dziel i zwycięŝaj! Zasada wyszukiwania połówkowego polega na podzieleniu posortowanego ciągu elementów na 2 części, zbadanie, w której z nich leży poszukiwany element i powtórzeniu tego algorytmu tylko dla tej części, w której on leży. Dzięki takiemu podziałowi uzyskujemy złożoność obliczeniową wyszukiwania O(log n), gdzie n ilość elementów ciągu. Zasadę tą stosujemy we wielu bardziej skomplikowanych algorytmach, np. podczas sortowania. Wykład 4. Strona 29.
Potęgowanie o wykładniku naturalnym Można zauważyć, iż w przypadku wykładników o większych potęgach można wykorzystać wcześniejsze wyniki mnożenia zamiast mnożenia podstawy n-1 razy: Wykład 4. Strona 30.
Obliczanie dnia tygodnia Dzień tygodnia może być obliczony bardzo efektywnie zastępując intuicyjne dodawanie od wybranej daty wzorami matematycznymi, które bez wykorzystania pętli obliczą stosowny dzień tygodnia z uwzględnieniem lat przestępnych, różnej ilości dni w poszczególnych miesiącach itp. Wykład 4. Strona 31.
Obliczanie wartości wielomianu Często prosta zamiana kolejności operacji i umiejętne wykorzystanie wspólnych wyników cząstkowych umożliwia zmniejszenie złożoności obliczeń. Wykład 4. Strona 32.
Literatura i bibliografia: L. Banachoi, K. Diks, W. Rytter: Algorytmy i struktury danych, WNT, Warszawa, 2001 Z. Fortuna, B. Macukow, J. Wąsoi, Metody numeryczne, WNT, Warszawa, 1993. K. Jakubczyk, Turbo Pascal i Borland C++, Wydanie II, Helion, 2006. J. i M. Jankowscy, Przegląd metod i algorytmów numerycznych, WNT, Warszawa, 1988. A. Kiełbasiński, H. Schwetlick, Numeryczna algebra liniowa, WNT, Warszawa 1992. A. Kierzkoi, Turbo Pascal. Ćwiczenia praktyczne., Helion 2006. K. Koleśnik, Wstęp do programowania z przykładami w Turbo Pascalu, Helion, M. Sysło: Elementy Informatyki. A. Szepietoi: Podstawy Informatyki. R. Tadeusiewicz, P. Moszner, A. Szydełko: Teoretyczne podstawy informatyki. W. M. Turski: Propedeutyka informatyki. N. Wirth: Wstęp do programowania systematycznego. N. Wirth: ALGORYTMY + STRUKTURY DANYCH = PROGRAMY. Wykład 4. Strona 33.