Zapis algorytmów: schematy blokowe i pseudokod 1 Przed przystąpieniem do napisania kodu programu należy ten program najpierw zaprojektować. Projekt tworzącego go algorytmu może być zapisany w formie schematu blokowego i/lub pseudokodu. 1. Schematy blokowe Schemat blokowy składa się z bloków instrukcji - dla różnych rodzajów instrukcji bloki mają różne kształty - oraz strzałek, które określają kolejność wykonywania tych instrukcji. START //Wejście //Operacje //Warunek NIE //operacje wykonywane w pętli TAK //Operacje //Wyjście STOP Rysunek 1: Model przykładowego schematu blokowego 1 Ćwiczenia audytoryjne z przedmiotu Podstawy informatyki, Wydział Fizyki i Informatyki Stosowanej AGH, 2017/2018. Elżbieta Wach (Elzbieta.Wach@fis.agh.edu.pl), http://galaxy.agh.edu.pl/~ewach/pi.html 1
1.1. Bloki 1.1.1. Blok początkowy Każdy algorytm rozpoczyna się blokiem początkowym. Wewnątrz znajduje się słowo START (ewentualnie START z nazwą procedury - jest to przydatne, gdy dany schemat blokowy będzie wykorzystywany wewnątrz innego jako podprogram: wówczas wystarczy go wywołać przy pomocy nazwy). Z bloku początkowego wychodzi dokładnie jedna strzałka. START START MINIMUM(a, b) Rysunek 2: Przykładowe bloki początkowe; drugi z nich rozpoczyna podprogram, który musi być uruchomiony z dwiema danymi wejściowymi: a i b 1.1.2. Blok końcowy Na końcu algorytmu znajduje się blok operacji STOP. Może pojawić się w schemacie więcej niż jeden raz. W przypadku zapisu schematu procedury, która będzie wykonywana w innym algorytmie, w bloku końcowym może znaleźć się również nazwa tej procedury lub słowo return jako informacja o powrocie do programu nadrzędnego. STOP STOP MINIMUM(a, b) RETURN Rysunek 3: Przykładowe bloki końcowe 1.1.3. Bloki wejścia/wyjścia Blok w kształcie równoległoboku odpowiada za operacje odczytu i zapisu danych. Można go również użyć w celu zapisu komunikatu dla użytkownika, np. dotyczącego błędu. Wczytaj a, b Rysunek 4: Przykładowy blok pobierania danych wejściowych 2
Wypisz x Zwróć x Wypisz BŁĄD: Podana liczba x jest ujemna! Rysunek 5: Przykładowe bloki wyjściowe 1.1.4. Blok operacji Większość instrukcji, które nie dotyczą wczytywania/wypisywania danych, zapisuje się w bloku operacji (tj. w prostokącie). Znajdą się tu np. operacje przypisania wartości do zmiennych. Kilka operacji występujących po sobie można zapisać w kolejnych linijkach jednego bloku. n 4 i 0 i i + 1 A[i] 2 (n 1) Rysunek 6: Przykładowy blok operacji 1.1.5. Blok procedury Jeżeli fragment algorytmu został zdefiniowany w osobnym schemacie, to możemy go wywołać w bloku procedury. Ten sposób zapisu algorytmu jest bardzo przydatny i wygodny w przypadku, gdy taki fragment musi być wykonywany kilkakrotnie. m MINIMUM(a, b) Rysunek 7: Przykładowy blok procedury 3
1.1.6. Blok decyzyjny Wewnątrz bloku decyzyjnego zapisujemy warunek. Z takiego bloku wychodzą dokładnie dwie strzałki: jedna prowadząca do operacji, które zostaną wykonane w przypadku spełnienia warunku, a druga wskazuje instrukcje dla sytuacji, w której warunek nie jest spełniony. x 8 NIE TAK Rysunek 8: Przykładowy blok decyzyjny 1.1.7. Łączniki Nawet stosunkowo krótki algorytm może być trudny do czytelnego przedstawienia w schemacie blokowym na kartce w sposób ciągły. Dlatego przydatne są łączniki, pozwalające na przeniesienie fragmentu schematu w inne miejsce na tej samej (łącznik wewnętrzny) lub na innej stronie (łącznik zewnętrzny). Łączniki numeruje się liczbami całkowitymi - dla każdego łącznika źródłowego istnieje jeden łącznik docelowy o tym samym numerze. 1 1 Rysunek 9: Przykładowy łącznik wewnętrzny - od lewej: źródłowy i docelowy 2 2 Rysunek 10: Przykładowy łącznik zewnętrzny - od lewej: źródłowy i docelowy 4
1.1.8. Punkty koncentracji Bloki są połączone strzałkami, które definiują kolejność wykonywania operacji. Jeżeli co najmniej dwie strzałki docierają do tego samego miejsca, dla czytelności zaznaczamy to punktem koncentracji. 1.2. Zmienne Rysunek 11: Punkt koncentracji Na temat zmiennych powiemy więcej w kontekście programowania w języku C. Póki co wystarczy informacja, że zmienną rozumiemy jako pewien obszar pamięci, w którym przechowujemy dane. Do zmiennej odnosimy się poprzez jej nazwę. Konkretna zmienna przechowuje wartości danego typu, np. liczby całkowite lub znaki. W algorytmie możemy zmieniać wartość zmiennej, ale musi ona pozostać tego samego typu. W schematycznym zapisie algorytmów założymy, że zmienna zaczyna istnieć od momentu, kiedy pojawia się po raz pierwszy - najczęściej w operacji przypisania jej wartości. 1.3. Zapis operacji W schematach blokowych nie powinno się używać zapisów odpowiednich dla konkretnego języka programowania - chcemy, aby zapis był uniwersalny i niezależny od języka. W rozdziale 2. podobną zasadę zastosujemy dla pseudokodu. Przypisanie wartości do zmiennej realizujemy przy pomocy strzałki skierowanej w stronę jej nazwy: n 4 m 2 n Rysunek 12: Operacja przypisania: od tej chwili wartość zmiennej n jest równa 4, natomiast zmienna m jest dwukrotnie większa Strzałka z grotami po obu stronach oznacza zamianę wartości: m n Rysunek 13: Operacja zamiany: od tej chwili wartość zmiennej n jest równa poprzedniej wartości m i odwrotnie; gdyby taki blok występował bezpośrednio po bloku z rys. 12, wówczas m byłoby równe 4, a n wynosiłoby 8 5
Najprostszą złożoną strukturą danych jest tablica, czyli kontener, który składa się z określonej liczby elementów. Każdy z tych elementów funkcjonuje jak osobna zmienna tego samego typu (np. w tablicy liczb całkowitych wszystkie elementy są liczbami całkowitymi). Dostęp do nich uzyskujemy przez odwołanie się do numeru elementu, czyli tzw. indeksu. Odwołanie takie realizujemy poprzez nazwę tablicy z indeksem zapisanym w nawiasie kwadratowym, np.: A[2]. Elementy tablicy numeruje się od 0 (chyba, że zdefiniowano inaczej) - wówczas dla tablicy o n elementach indeksy należą do zbioru {0, 1,..., n 1}. Liczba elementów tablicy jest równa length[a] - jest to parametr danej tablicy. A [length[a] 1] 0 Rysunek 14: Operacja przypisania zera do ostatniego elementu tablicy A 1.4. Warunki W blokach decyzyjnych używamy warunków w zwykłym zapisie matematycznym, tj. korzystając z operatorów =,, <, >,,,,. START Wczytaj a (a > 0) (a mod 2 1) TAK NIE Wypisz Podana liczba jest dodatnia i parzysta STOP Rysunek 15: Algorytm z warunkiem 6
1.5. Pętle Pętle realizujemy przy użyciu bloku warunkowego. W pseudokodzie oraz większości języków programowania mamy do dyspozycji kilka rodzajów pętli: jedną z nich jest pętla while, której działanie jest wyjaśnione w podpisie do rys. 16. Identyczny jest zapis blokowy pętli for (patrz: rozdział 2.3). 1.5.1. Pętle for i while START i 1 s 0 i i + 1 s s + a i 5 TAK Wczytaj a NIE Zwróć s STOP Rysunek 16: Algorytm z pętlą while sumujący pięć wczytanych liczb. Dopóki spełniony jest warunek, że i jest mniejsze lub równe 5, wykonywane są operacje wskazane przez strzałkę z napisem TAK. Po zakończeniu pętli suma jest zwracana i następuje koniec algorytmu. Gdybyśmy zapomnieli o przypisywaniu nowej, większej wartości do zmiennej i wewnątrz pętli, wówczas pętla byłaby nieskończona. Przedstawiona tu pętla while jest równoznaczna z pętlą for, której zmienna sterująca i przebiega od 1 do 5 (patrz rozdział 2.3, dotyczący zapisu pętli w pseudokodzie). 7
1.5.2. Pętla repeat...until Jeśli zamienimy kolejnością blok warunkowy z instrukcjami pętli (rys. 17), będziemy mieli do czynienia z tzw. pętlą repeat...until 2. Różnica w stosunku do poprzedniego zapisu polega na tym, że operacje tej pętli wykonają się co najmniej raz, ponieważ jest to niezależne od warunku. START i 1 s 0 Wczytaj a i i + 1 s s + a i 5 TAK NIE Zwróć s STOP Rysunek 17: Algorytm z pętlą repeat...until sumujący pięć wczytanych liczb. Dopóki spełniony jest warunek i 5, wykonywane są operacje wskazane przez strzałkę z napisem TAK. Jaka jest różnica między pętlą z rys. 16 a pętlą z rys. 17 z praktycznego punktu widzenia? Załóżmy, że na początku algorytmów (zaraz po bloku START ) do i przypisujemy wartość 10, a nie 0. Pierwszy z nich (z pętlą while) stwierdzi, że warunek i 5 nie jest spełniony, więc od razu przejdzie do bloku Zwróć s i się zakończy. Końcową wartością s będzie 0. Natomiast drugi z algorytmów (z pętlą repeat...until) najpierw jeden raz wczyta a, doda tę wartość do s i dopiero wtedy sprawdzi warunek - tutaj zwróci s i zakończy działanie. 2 W językach C i C++ podobną instrukcją jest pętla do...while. 8
2. Pseudokod Pseudokod, w przeciwieństwie do większości najpopularniejszych języków programowania, nie posiada standardu. Opisany w tym rozdziale formalizm został zaczerpnięty z książki pt. Wprowadzenie do algorytmów autorstwa Thomasa H. Cormena, Charlesa E. Leisersona i Ronalda L. Rivesta (Wydawnictwa Naukowo-Techniczne, wyd. czwarte; Warszawa 2001). Rozpoczynamy od zapisu nazwy algorytmu (pozwoli na wywołanie go wewnątrz innego). Razem z nazwą w nawiasie podajemy dane wejściowe, np. Min2(a, b) oznacza algorytm o nazwie Min2, który przyjmuje dwa argumenty wejściowe: a i b. Jeżeli algorytm zwraca jakąś wartość, wówczas kończy się słowem kluczowym return. Dla przejrzystości algorytmu w zapisie stosuje się numerowanie linii oraz wcięcia, np.: Min2(a, b) 1. if a < b 2. then return a 3. else return b 2.1. Zapis operacji Formalizm zapisu przypisania wartości do zmiennej (i 0), zamiany wartości zmiennych (a b), warunków (x 0 k 1), odnoszenia się do elementów tablicy (tab[k]) stosujemy taki sam, jak w przypadku schematów blokowych (patrz: rozdział 1). Wywołanie algorytmu wewnątrz innego kodu wykonujemy przy użyciu jego nazwy. Jeśli metoda (algorytm będący podprogramem) zwraca wartość, możemy ją od razu przypisać do zmiennej, np.: m Min2(2, x). 2.2. Instrukcje warunkowe W pseudokodzie sprawdzenie warunku zapisuje się za pomocą słów kluczowych if, then oraz else. Przykładowy algorytm Warunki(x) sprawdza, czy dana wejściowa x jest dodatnia, ujemna, czy jest zerem, następnie wypisuje komunikat i zwraca odpowiednio: 1, -1 lub 0. Warunki(x) 1. i 0 2. if x = 0 3. then Wypisz Liczba x jest równa 0. 4. else if x > 0 5. then i 1 6. Wypisz Liczba x jest dodatnia. 7. else i 1 8. Wypisz Liczba x jest ujemna. 9. return i 9
2.3. Pętle 2.3.1. Pętla for Pętla for wymaga zdefiniowania zmiennej sterującej tą pętlą, która zaczyna się od wskazanej wartości i wewnątrz pętli jest automatycznie zwiększana lub zmniejszana o 1. Pętla kończy się, gdy ta zmienna osiągnie wartość określoną słowem kluczowym to lub downto. Użycie to spowoduje automatyczne dodawanie jedynki do zmiennej sterującej, podczas gdy downto oznacza odejmowanie jedynki. Petla_for() 1. s 0 2. for i 1 to 5 3. do Wczytaj a 4. s s + a 5. return s Powyższy algorytm jest identyczny ze schematem blokowym z rys. 16. Korzystając z pętli for w pseudokodzie nie uwzględniamy linijki i i + 1 (odbywa się to automatycznie). W algorytmie Petla_for() zmienna i jest tworzona w drugiej linii. Oznacza to, że jest lokalna - poza tym algorytmem nie jest widoczna. Nie jest nam to potrzebne, ponieważ jest tylko zmienną pomocniczą. 2.3.2. Pętla while Pętla while wymaga użycia dwóch słów kluczowych (while i do) oraz warunku. Jeśli warunek jest spełniony, wówczas instrukcje wewnątrz pętli są wykonywane. Petla_while() 1. i 1 2. s 0 3. while i 5 4. do Wczytaj a 5. i i + 1 6. s s + a 7. return s Powyższy przykład zapisuje taki sam algorytm, jak Petla_for(), ale przy użyciu innego rodzaju pętli. Tym razem musimy pamiętać o zapisaniu instrukcji zmiany wartości i w pętli (linijka 5), poza tym przypisanie początkowej wartości i 1 należy umieścić przed pętlą. Ten sam algorytm zapisany przy pomocy pętli for jest krótszy, a zmienna sterująca pętlą zmienia się automatycznie. Dlaczego więc czasem warto użyć pętli while? Jej przewaga nad for jest widoczna wtedy, gdy chcemy, by zmienna sterująca pętli była edytowana w inny sposób, niż proste zwiększanie lub 10
zmniejszanie o 1. Ponadto w pętli while możemy wpisać jakikolwiek warunek, niekoniecznie związany ze zmienną tego typu, co i w poprzednich przykładach. While_true() 1. while True 2. do Wczytaj a 3. if a > 0 4. then return a Pętla w algorytmie While_true() będzie się wykonywała aż do momentu, gdy wczytana liczba a będzie dodatnia. Uwaga: takie użycie pętli while może prowadzić do nieskończonego algorytmu, więc należy zachować szczególną ostrożność - przede wszystkim pamiętać o warunku zakończenia, który w tej sytuacji musi być zapisany wewnątrz pętli. 2.3.3. Pętla repeat...until Pętla repeat...until jest wykonywana, gdy warunek nie jest spełniony (inaczej niż w while), ponadto pętla zawsze wykona się co najmniej jeden raz. Petla_repeat() 1. i 1 2. s 0 3. repeat Wczytaj a 4. i i + 1 5. s s + a 6. until i = 6 7. return s Powyższy algorytm jest równoważny schematowi blokowemu z rys. 17. 2.3.4. Pętla z tablicą Zapisując algorytmy z tablicami, często będziemy korzystać z pętli. Poniżej znajdują się przykładowe algorytmy zwracające minimalną wartość tablicy wejściowej A o length[a] elementach; zakładamy numerowanie elementów tablicy od indeksu równego 0. MinT_for(A) 1. m A[0] 2. for i 1 to length[a] 1 3. do m Min2(m, A[i]) // Wywołany tu algorytm Min2(m, A[i]) znajduje się na stronie 9. 4. return m 11
MinT_while(A) 1. m A[0] 2. i 1 3. while i length[a] 1 4. do m Min2(m, A[i]) 5. i i + 1 6. return m MinT_repeat(A) 1. m A[0] 2. i 1 3. repeat m Min2(m, A[i]) 4. i i + 1 5. until i length[a] 6. return m 12