Login lub e-mail Hasło   

Miejsca zerowe funkcji - Wstęp

Odnośnik do oryginalnej publikacji: http://www.i-lo.tarnow.pl/edu/inf/alg/zm(...)ex.html
Miejscem zerowym funkcji f ( x ) będziemy nazywali argument x o , dla którego funkcja przyjmuje wartość 0: x o jest miejscem zerowym wtedy i tylko wt...
Wyświetlenia: 10.538 Zamieszczono 04/11/2006

Miejscem zerowym funkcji f(x) będziemy nazywali argument xo, dla którego funkcja przyjmuje wartość 0:

xo jest miejscem zerowym wtedy i tylko wtedy, gdy f(xo) = 0
   
   
   

Często uczniowie (nawet ci lepsi) mylą to proste pojęcie i twierdzą, iż miejsce zerowe to argument xo = 0. Prawdopodobnie zamęt wprowadza indeks przy literce x. Zastanów się, jeśli by tak było, to po co potrzebna jest nam ta cała procedura znajdowania czegoś, co jest przecież równe 0...?

 
       

Funkcja f(x) = 2x - 4 posiada miejsce zerowe dla xo = 2, ponieważ:

 f(xo)  = 2xo - 4
 f(2)  = 2 2 - 4 = 4 - 4 = 0

Miejsce zerowe często nazywamy pierwiastkiem funkcji.

Funkcja może posiadać więcej niż jeden pierwiastek:

Funkcja f(x) = x2 - 1 posiada dwa pierwiastki: xo = -1 oraz xo = 1, gdyż:

 f(xo)  = (xo)2 - 1
 f(-1)  = (-1)2 - 1 = 1 - 1 = 0
  f(1)  = 12 - 1 = 1 - 1 = 0

Funkcja może posiadać nieskończenie wiele pierwiastków:

Funkcja f(x) = sin(x - 2) posiada pierwiastki dla każdego xo = + 2, gdzie k = 0, ±1, ±2 ...

 


Graficznie miejsce zerowe funkcji możemy interpretować jako punkt przecięcia osi współrzędnych OX przez wykres funkcji:

 

Znajdowanie miejsc zerowych ma olbrzymie znaczenie w matematyce, fizyce, astronomii, technice itp. Dlatego już dawno temu matematycy opracowali wiele metod rozwiązywania tego zagadnienia. Zasadniczo istnieją dwa podejścia:

  • zastosowanie metod analitycznych do znalezienia pierwiastka funkcji. Żmudne i wymagające dobrej znajomości matematyki - szczególnie, gdy funkcja ma bardzo skomplikowany przepis. Zaletą metody analitycznej jest to, iż otrzymujemy rozwiązanie dokładne. Wadą jest brak ogólnej metody znajdowania pierwiastków.
  • wyliczenie pierwiastka przybliżonego z założoną dokładnością - wraz z rozwojem komputerów metody numeryczne zyskały na popularności. W technice często nie potrzebujemy super dokładnej wartości pierwiastka, lecz zadowalamy się jego przybliżeniem. Zaletą tych metod w porównaniu do metod analitycznych jest ogólność i prostota.

W obliczeniach komputerowych będziemy wykorzystywali liczby rzeczywiste. Takie liczby są kodowane w specjalnym systemie zwanym binarnym kodem zmiennoprzecinkowym (ang. binary floating point code). Dokładny opis systemów zmiennoprzecinkowych znajdziesz w artykule o binarnym kodowaniu liczb. Cechą charakterystyczną kodów binarnych stosowanych w obliczeniach komputerowych jest ich skończoność. Oznacza to, iż do reprezentacji liczby komputer wykorzystuje stałą liczbę bitów (np. 8, 16, 32, 64 itp.). W wyniku kod binarny może reprezentować ograniczoną ilość liczb. Na przykład kod naturalny 8-bitowy może przedstawić tylko 256 różnych liczb poczynając od 0, a kończąc na 255. Oto proste przykłady:

Delphi 7.0 Personal Edition

// Program demonstruje przekroczenie zakresu liczby 8-bitowej
//-----------------------------------------------------------

program test;

{$APPTYPE CONSOLE}

begin
writeln(byte(257)); readln;
end.

Borland C++ Builder 6.0 Personal Edition

// Program demonstruje przekroczenie zakresu liczby 8-bitowej
//-----------------------------------------------------------

#include <iostream>

using namespace std;

int main(int argc, char* argv[])
{
cout << (unsigned)(unsigned char) 257 << endl << endl;
system("pause");
return 0;
}

Program powinien wypisać liczbę 257, wypisuje natomiast 1. Dlaczego tak się dzieje? Otóż liczba 257 musi być reprezentowana przez 9 bitów:

100000001(2) = 257(10).

Jednakże kod ten obcinamy do 8 bitów (to właśnie dokonują konwersje zastosowane w programach). W efekcie otrzymujemy:

100000001(2) = 00000001(2) = 1(10).


W przypadku liczb zmiennoprzecinkowych istotnym parametrem jest precyzja, czyli dokładność przedstawienia liczby. Określa ona ile początkowych cyfr liczby zostanie zapamiętanych dokładnie. Na przykład kod zmiennoprzecinkowy 32-bitowy zapamiętuje dokładnie tylko 7-8 początkowych cyfr liczby. Pozostałe cyfry są już niewiarygodne. Oto proste przykłady:

Borland Delphi 7.0 Personal Edition

// Program demonstruje przekroczenie precyzji liczby zmiennoprzecinkowej
//----------------------------------------------------------------------

program Project2;

{$APPTYPE CONSOLE}

var
a : single;
begin
a := 10000000123; // Końcówka ...123 nie zostanie zapamiętana!!!
writeln(a:12:0);
readln;
end.

Borland C++ Builder 6.0 Personal Edition

// Program demonstruje przekroczenie precyzji liczby zmiennoprzecinkowej
//----------------------------------------------------------------------

#include <iostream>
#include <iomanip>

using namespace std;

int main(int argc, char* argv[])
{

float a = 10000000123; // Końcówka ...123 nie zostanie zapamiętana!!!

cout.precision(0); // 0 cyfr po przecinku
cout.setf(ios::fixed); // format stałoprzecinkowy
cout << a << endl << endl;
system("pause");
return 0;
}

W powyższych przykładach zmienna a zapamiętuje tylko 8 najbardziej znaczących cyfr liczby. Dlatego cyfry końcowe 123 nie zostają poprawnie wyświetlone - liczba jest pamiętana przez komputer jako liczba przybliżona. Oczywiście można przyjąć do zapisu liczb system 64-bitowy (double). Wtedy precyzja wzrośnie do około 15 cyfr najbardziej znaczących, ale problem wciąż będzie występował, tyle że na dalszym miejscu.


Komputer zapamiętuje liczby ze skończoną dokładnością. Konsekwencje są olbrzymie! Spójrz na poniższe dwa przykłady:

Borland Delphi 7.0 Personal Edition

// Program demonstruje błędy zaokrągleń
//-------------------------------------

program Test;

{$APPTYPE CONSOLE}

var
a : double;
begin
a := 0.1;
if a * 10 = 1 then
writeln('WSZYSTKO OK :)')
else
writeln('NIC NIE JEST OK :(');
readln;
end.

Borland C++ Builder 6.0 Personal Edition

// Program demonstruje błędy zaokrągleń
//-------------------------------------

#include <iostream>

using namespace std;

int main(int argc, char* argv[])
{
double a = 0.1;

cout << ((a * 10 == 1) ? "WSZYSTKO OK :)" : "NIC NIE JEST OK :(");
cout << endl << endl;
system("pause");
return 0;
}

Program tworzy zmienną a i zapamiętuje w niej wartość 0.1. Następnie sprawdza, czy zawartość zmiennej a pomnożona przez 10 daje 1. Jeśli tak, wypisuje tekst "WSZYSTKO OK :)". Analizując ten program pod kątem matematycznym dochodzimy do wniosku, iż tekst ten powinien się pojawić, ponieważ z arytmetycznego punktu widzenia iloczyn 0.1 przez 10 daje w wyniku właśnie 1. Prawda? Otóż nie.

Komputer pracuje wewnętrznie w systemie dwójkowym. Ułamek dziesiętny 0.1 posiada w systemie dwójkowym następujące rozwinięcie (sposób konwersji liczby dziesiętnej na dwójkową znajdziesz w artykule o binarnym kodowaniu liczb):

0.1(10) = 0.0001100110011001100110011001100110011001100110011001100...(2)

Jest to zatem ułamek okresowy nieskończony (podobną własność posiada ułamek 1/3 w systemie dziesiętnym). Ponieważ zapis zmiennoprzecinkowy ograniczony jest wewnętrznie do skończonej liczby bitów (cyfr rozwinięcia dwójkowego), komputer zapamięta ułamek 0.1 z przybliżeniem, zatem niedokładnie. Oczywiście błąd zaokrąglenia jest bardzo mały, ale jest!

Teraz taką przybliżoną wartość mnożymy przez 10. Czy otrzymamy 1? Nie, otrzymamy liczbę bardzo bliską 1, lecz różną od 1. Dla komputera jest to już zupełnie inna liczba, zatem porównanie daje wynik negatywny i w efekcie komputer wyświetli ten drugi napis "NIC NIE JEST OK :(".

Zwróć uwagę, iż nawet w tak prostych sytuacjach mogą pojawić się problemy. Jest to swego rodzaju pułapka na niedouczonych programistów i jakże często w nią wpadają!

   
   
   

Wartości zmiennoprzecinkowych (szczególnie uzyskanych w wyniku obliczeń) nie wolno ze sobą bezpośrednio porównywać za pomocą operatora równości.

 
       

Co zatem mamy zrobić? Otóż zamiast przyrównywać wartość zmiennoprzecinkową do innej wartości będziemy sprawdzać, czy obie są sobie dostatecznie bliskie. Innymi słowy należy sprawdzić, czy jedna wartość zmiennoprzecinkowa wpada w dostatecznie małe otoczenie drugiej. W tym celu wystarczy przetestować wartość bezwzględną ich różnicy (interpretowaną jako odległość pomiędzy nimi):

Wartość a jest równa wartości b z dokładnością do ε wtedy i tylko wtedy, gdy:

| a - b | < ε

Zapamiętaj to proste rozwiązanie, gdyż znakomicie ułatwi ci ono poruszanie się w gąszczu wyników zmiennoprzecinkowych. Podane poprzednio programy należy zmodyfikować następująco:

Borland Delphi 7.0 Personal Edition

program Test;

{$APPTYPE CONSOLE}

var
a : double;
begin
a := 0.1;
if abs(a * 10 - 1) < 0.00000001 then
writeln('WSZYSTKO OK :)')
else
writeln('NIC NIE JEST OK :(');
readln;
end.

Borland C++ Builder 6.0 Personal Edition

#include <iostream>
#include <math>

using namespace std;

int main(int argc, char* argv[])
{
double a = 0.1;

cout << (fabs(a * 10 - 1) < 0.00000001 ? "WSZYSTKO OK :)" : "NIC NIE JEST OK :(");
cout << endl << endl;
system("pause");
return 0;
}
 
Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.

Podobne artykuły


13
komentarze: 2 | wyświetlenia: 22990
10
komentarze: 2 | wyświetlenia: 8178
58
komentarze: 20 | wyświetlenia: 80733
49
komentarze: 18 | wyświetlenia: 65102
31
komentarze: 8 | wyświetlenia: 57050
12
komentarze: 3 | wyświetlenia: 29830
37
komentarze: 9 | wyświetlenia: 28636
27
komentarze: 10 | wyświetlenia: 20417
23
komentarze: 5 | wyświetlenia: 19714
21
komentarze: 14 | wyświetlenia: 66222
48
komentarze: 31 | wyświetlenia: 8943
14
komentarze: 8 | wyświetlenia: 100935
17
komentarze: 4 | wyświetlenia: 14482
15
komentarze: 5 | wyświetlenia: 32852
 
Autor
Artykuł




Brak wiadomości


Dodaj swoją opinię
W trosce o jakość komentarzy wymagamy od użytkowników, aby zalogowali się przed dodaniem komentarza. Jeżeli nie posiadasz jeszcze swojego konta, zarejestruj się. To tylko chwila, a uzyskasz dostęp do dodatkowych możliwości!
 

© 2005-2018 grupa EIOBA. Wrocław, Polska