Wróć do informacji o e-podręczniku Wydrukuj Pobierz materiał do PDF Pobierz materiał do EPUB Pobierz materiał do MOBI Zaloguj się, aby dodać do ulubionych Zaloguj się, aby skopiować i edytować materiał Zaloguj się, aby udostępnić materiał Zaloguj się, aby dodać całą stronę do teczki

Co oznacza poprawność algorytmu?

To, co intuicyjnie rozumiemy jako poprawność algorytmu, formalnie określamy mianem całkowitej poprawnościalgorytm całkowicie poprawnycałkowitej poprawności. Składają się na nią dwa elementy: własność stopu oraz częściowa poprawność.

Aby wyjaśnić znaczenie tych terminów, rozważmy najpierw pojęcie specyfikacji algorytmu. Określamy tak ścisły opis danych wejściowych, danych wyjściowych oraz relacji, jakie między nimi zachodzą.

Algorytm powinien działać w sposób poprawny dla danych, które spełniają warunek początkowy – w tej sytuacji zwróci on dane wyjściowe spełniające warunek końcowy, czyli wynik osiągnięty w sposób opisany przez specyfikację. Nie oznacza to, że algorytm nie będzie działał dla danych niespełniających warunku początkowego - po prostu nie jest on do nich przystosowany i nie można na nim polegać.

Częściowa poprawnośćczęściowa poprawność algorytmuCzęściowa poprawność oznacza, że jeżeli algorytm zakończy się wykonywać, a dane wejściowe spełniały warunek początkowy, to zwrócony przez niego wynik będzie poprawny. Własność stopuwłasność stopuWłasność stopu oznacza, że dla każdych danych wejściowych spełniających warunek początkowy algorytm skończy się wykonywać po skończonej liczbie kroków.

Algorytm jest całkowicie poprawny, jeżeli jest jednocześnie częściowo poprawny oraz ma własność stopu. Oznacza to, że jeżeli otrzymał on poprawne dane, zwróci poprawny wynik po skończonej liczbie kroków.

R1RjKbcOKDX5m
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Zagadnienie poprawności omówmy najpierw na podstawie następującego algorytmu.

Specyfikacja problemu:

Dane:

  • n – liczba elementów w tablicy; liczba naturalna

  • tab – tablica n liczb całkowitych

Wynik:

Algorytm zwraca największą liczbę występującą w tablicy.

Przedstawiony na schemacie blokowym algorytm wygląda następująco:

R1aYsvxpWYFZ81
Źródło: Contentplus.pl Sp. z o.o., licencja: CC BY-SA 3.0.

Ten sam algorytm możemy przedstawić również za pomocą pseudokodu:

Linia 1. tab otwórz nawias kwadratowy 1 kropka kropka n zamknij nawias kwadratowy ← wprowadź n liczb całkowitych do tablicy. Linia 2. i ← 2. Linia 3. maksimum ← tab otwórz nawias kwadratowy 1 zamknij nawias kwadratowy. Linia 4. dopóki i ≤ n wykonuj dwukropek. Linia 5. jeżeli tab otwórz nawias kwadratowy i zamknij nawias kwadratowy zamknij nawias ostrokątny maksimum dwukropek. Linia 6. maksimum ← tab otwórz nawias kwadratowy i zamknij nawias kwadratowy. Linia 7. i ← i plus 1. Linia 8. zwróć maksimum.

Dowodzenie poprawności

Przeprowadzenie dowodu na całkowitą poprawność sprowadza się do udowodnienia częściowej poprawności oraz własności stopu.

By sprawdzić poprawność algorytmu, dzielimy go na fragmenty. Za pomocą zapisów matematycznych sprawdzamy, czy dane wejściowe dają odpowiednie dane wyjściowe.  Uzyskany końcowy wynik powinien być zgodny ze specyfikacją.

Aby przybliżyć istotę tych dowodów, przeanalizujmy działanie podanego wyżej algorytmu dla przykładowych danych:

iteracja

tab

i

maksimum

przed iteracją 1

[1,2,3,9,10,6,4,1,2,7]

2

1

po iteracji 1

[1,2,3,9,10,6,4,1,2,7]

3

2

po iteracji 2

[1,2,3,9,10,6,4,1,2,7]

4

3

po iteracji 3

[1,2,3,9,10,6,4,1,2,7]

5

9

po iteracji 4

[1,2,3,9,10,6,4,1,2,7]

6

10

po iteracji 5

[1,2,3,9,10,6,4,1,2,7]

7

10

po iteracji 6

[1,2,3,9,10,6,4,1,2,7]

8

10

po iteracji 7

[1,2,3,9,10,6,4,1,2,7]

9

10

po iteracji 8

[1,2,3,9,10,6,4,1,2,7]

10

10

po iteracji 9

[1,2,3,9,10,6,4,1,2,7]

11

10

Na koniec zostaje zwrócona wartość zmiennej maksimum.

Możemy zauważyć, że zwrócona przez algorytm wartość jest poprawna – liczba jest rzeczywiście największa spośród przykładowych danych wejściowych.

Nic nie stoi na przeszkodzie, abyśmy sprawdzili działanie tego algorytmu dla innego zestawu danych. Nieskończenie wiele razy moglibyśmy sprawdzać, co zostanie zwrócone przez algorytm, a i tak nie mielibyśmy stuprocentowej pewności, że ten algorytm jest poprawny. Zawsze bowiem może okazać się, że pominęliśmy pewien skrajny przypadek – sytuację, w której nasz algorytm zwróci niepoprawny wynik, lub (co gorsze) nigdy nie przestanie się wykonywać. Właśnie dlatego przywiązuje się dużą uwagę do poprawności algorytmów i przeprowadza się dowód na całkowitą poprawność uwzględniający wszystkie przypadki zgodne ze specyfikacją algorytmu.

Zachodzi tu pewna prawidłowość – wartość zmiennej maksimum nigdy się nie zmniejsza. Co więcej, możemy stwierdzić, że jej wartość w każdym wykonaniu pętli będzie równa największej liczbie napotkanej do tej pory, czyli w elementach tablicy o indeksach od do i.

W momencie, w którym pętla kończy się wykonywać, zmienna maksimum zawiera największą wartość spośród elementów tablicy o indeksach od do , czyli największą wartość w całej tablicy.

Taką zależność nazywamy niezmiennikiem pętliniezmiennik pętliniezmiennikiem pętli. Jest to warunek, który jeśli jest spełniony przed wykonaniem iteracji, będzie spełniony również po niej. Może zostać on użyty w tzw. metodzie niezmienników, która jest jednym ze sposobów na udowodnienie częściowej poprawności algorytmu.

Znalezienie niezmiennika pętli może być problematyczne nawet w relatywnie prostych algorytmach. Wynika to z faktu, że nie jest on oczywisty na pierwszy rzut oka. Często znalezienie takiego niezmiennika wymaga przeanalizowania działania algorytmu na kilku zestawach danych wejściowych, a następnie wyciągnięcie odpowiednich wniosków.

Ciekawostka

Użycie niezmienników pętli to forma dowodu indukcyjnego. Składa się on z dwóch kroków: kroku bazowego oraz kroku indukcyjnego. W kroku bazowym udowadniamy, że pewna teza  jest prawdziwa dla , tj. jest prawdziwe. Krok indukcyjny polega na wykazaniu, że jeżeli teza jest prawdziwa dla pewnej liczby naturalnej , to teza również musi być prawdziwa.

Wiemy już, że wartość zmiennej maksimumzawiera w każdej iteracji pętli największą wartość w pewnym przedziale, więc po wyjściu z pętli będzie ona zawierała największą wartość w całej tablicy. Pytanie brzmi: czy pętla kiedykolwiek się zakończy?

W przypadku omawianego algorytmu własność stopu możemy udowodnić w następujący sposób:

  1. Algorytm skończy się wykonywać, kiedy i > n.

  2. n ma określoną wartość.

  3. W każdej iteracji pętli wartość zmiennej i zwiększa się o .

  4. Zatem algorytm zakończy się po skończonej liczbie iteracji – tym samym udowodniliśmy własność stopu.

Ważne!

Zwróć szczególną uwagę na dokładność naszego dowodu. Samo zwiększanie zmiennej nie wystarczy, aby dowód był prawidłowy, musi się ona zwiększać o stałą wartość. Tak samo liczba n, użyta w krokach pierwszym i drugim, musi być stała i skończona. Inaczej nasz dowód nie będzie wystarczający.

Sprawdziliśmy, czy nasz algorytm posiada własność stopu – zatrzymuje się w momencie, w którym wartość zmiennej i przekracza wartość n. Korzystając z niezmiennika pętli, wykazaliśmy również jego częściową poprawność. Możemy zatem stwierdzić, że nasz algorytm jest całkowicie poprawny – za tym stwierdzeniem stoi pewna forma dowodu matematycznego.

Dowody na całkowitą poprawność bardziej skomplikowanych algorytmów będą oczywiście odpowiednio dłuższe, lecz na poziomie szkoły ponadpodstawowej analiza na takim poziomie jest wystarczająca.

Przykład algorytmu – obliczanie silni

Silnię liczby naturalnej  n definiujemy jako iloczyn wszystkich liczb naturalnych od do n, zaś zapisujemy jako n!. Aby obliczyć jej wartość dla pewnego n, możemy wykorzystać następujący algorytm:

Specyfikacja problemu:

Dane:

  • n – liczba, której silnię obliczamy; liczba naturalna

Wynik:

Algorytm zwraca wartość silni liczby n.

Zapis algorytmu za pomocą pseudokodu:

Linia 1. n ← wprowadź liczbę naturalną n. Linia 2. wynik ← 1. Linia 3. i ← 2. Linia 4. dopóki i ≤ n wykonuj dwukropek. Linia 5. wynik ← wynik asterysk i. Linia 6. i ← i plus 1. Linia 7. zwróć wynik.

Rozważmy działanie tego algorytmu dla liczby n = 5.

iteracja

n

wynik

i

przed iteracją 1

5

1

2

po iteracji 1

5

2

3

po iteracji 2

5

6

4

po iteracji 3

5

24

5

po iteracji 4

5

120

6

Na końcu zostaje zwrócony wynik , co rzeczywiście jest silnią z liczby .

W przypadku tego algorytmu, własność stopu jest analogiczna, jak w algorytmie wyszukiwania największej liczby w tablicy, tj. zmienna i zwiększa się o stałą wartość, więc po skończonej liczbie iteracji przekroczy zmienną n, która również jest wartością stałą i skończoną.

Kluczem do niezmiennika pętli w tym algorytmie jest zmienna wynik. Jest to bowiem silnia z liczby i. Możemy na podstawie tych stwierdzeń udowodnić poprawność tego algorytmu, jak również zademonstrować ją na przykładzie. Możemy też wyjaśnić krok po kroku, dlaczego nasz algorytm zwraca poprawne wyniki.

Inne cechy algorytmów

Poza opisanymi, wynikającymi z definicji, właściwościami całkowitej poprawności, możemy wymienić jeszcze kilka cech poprawności algorytmu.

Aspektem algorytmu, który przyjmujemy za oczywisty, jest jego określoność. Oznacza to, że czytelnik musi być w stanie zrozumieć i dokładnie odwzorować kolejne etapy wykonywania algorytmu. Aby algorytm był niepoprawnie określony, wystarczy jedna błędna instrukcja. Przykładowo: „wczytaj liczby” nie mówi nam nic o tym, jakie liczby mamy wczytać – całkowite, rzeczywiste, zespolone...? Ile liczb trzeba wczytać? W tej instrukcji powinniśmy zawrzeć konkrety, przez co mogłaby ona przyjąć postać, np. „wczytaj liczbę całkowitą X oraz liczbę naturalną N”.

Określoność algorytmu umożliwia wykorzystanie komputera, aby zweryfikować, czy faktycznie zwraca on poprawne wyniki dla konkretnych danych wejściowych. Musimy się jednak liczyć z tym, że zapis algorytmu w języku programowania wiąże się z ryzykiem wprowadzenia błędów składniowych, które uniemożliwiają uruchomienie programu, dopóki nie zostaną naprawione, oraz błędów logicznych, wynikających z niepoprawnego przełożenia algorytmu na wybrany język programowania.

Inną ważną cechą algorytmu jest jego uniwersalność. Oznacza ona, że algorytm powinien działać dla dowolnych danych spełniających warunki specyfikacji. Dotyczy to również liczby danych wejściowych, np. algorytm ma działać dla ciągu n–wyrazowego, a nie dla ciągu zawierającego z góry określoną liczbę wyrazów.

Na szczególną uwagę zasługuje wspomniana wcześniej skończoność algorytmu. W praktyce nie wystarczy, że algorytm kiedyś skończy się wykonywać – chcemy, aby stało się to jak najszybciej, przy jak najmniejszej liczbie kroków lub zużywając jak najmniej zasobów takich jak pamięć, w której przechowywane są zmienne. Dobrze zaprojektowane algorytmy wykonują tylko niezbędne operacje – innymi słowy są efektywne.

Słownik

algorytm całkowicie poprawny
algorytm całkowicie poprawny

algorytm, który jest zarówno częściowo poprawny, jak i posiada własność stopu

częściowa poprawność algorytmu
częściowa poprawność algorytmu

własność algorytmu, która mówi, że dla poprawnych danych wejściowych, jeżeli algorytm zakończy się wykonywać po skończonej liczbie kroków, to zwróci on poprawne dane wyjściowe

niezmiennik pętli
niezmiennik pętli

zdanie logiczne, które jeśli jest prawdziwe przed wykonaniem iteracji pętli, będzie prawdziwe również po jej wykonaniu

własność stopu
własność stopu

cecha algorytmu, która mówi, że dla poprawnych danych wejściowych algorytm zatrzymuje się w skończonym czasie