background image

53. Arytmetyka stałopozycyjna i zmiennopozycyjna.

Zapis stałoprzecinkowy albo stałopozycyjny – jeden ze sposobów zapisu liczb ułamkowych 
stosowanych w informatyce. Do zapisu liczby stałoprzecinkowej przeznaczona jest z góry 
określona ilość cyfr dwójkowych, a pozycję przecinka ustala się arbitralnie, w zależności od 
wymaganej dokładności.

Podziału na część całkowitą i ułamkową dokonuje projektant systemu lub programista, który 
przewiduje z jak dużymi liczbami całkowitymi lub z jak dużą dokładnością obliczenia będą 
wykonywane. Zwiększanie precyzji liczby to zmniejszanie zakresu, gdyż bity które mają 
reprezentować część ułamkową (stać za przecinkiem) nie mogą już reprezentować wartości 
całkowitych. Stwierdzenie odwrotne również jest prawdziwe: zwiększanie zakresu 
(całkowitoliczbowego) to zmniejszanie precyzji (mniej bitów do dyspozycji na opisanie 
części ułamkowej).

W skrajnych wypadkach możliwa jest sytuacja kiedy przecinek będzie stał poza znaczącymi 
cyframi liczby. Jeśli będzie on z lewej strony, będziemy mieć zakres mniejszy od 1, jeśli z 
prawej – precyzja będzie większa lub równa 1 (równość wystąpi, kiedy przecinek będzie stał 
bezpośrednio za cyframi znaczącymi, będziemy mieć wtedy zwykłe liczby całkowite).

Zakresy liczb

Wartość liczby stałoprzecinkowej jest określana tak jak w pozycyjnym systemie dwójkowym. 
Wagi bitów części całkowitej mają wartości (kolejno, od najbardziej znaczącego bitu): 

, natomiast wagi bitów części ułamkowej mają wartości: 

Dokładność reprezentacji wynosi 2 

− n

, czyli jest równa wadze najmniej znaczącego bitu 

części ułamkowej.

Na przykład jeśli na część całkowitą zostaną przeznaczone 4 bity (k = 4), natomiast na część 
ułamkową 2 bity (n = 2), wówczas:

wartość maksymalna:

1111,11

2

 = 2

3

 + 2

2

 + 2

1

 + 2

0

 + 2

-1

 + 2

-2

 = 15,75

10

wartość minimalna:

0000,01

2

 = 2

-2

 = 0,25

10

przykładowa liczba:

1011,10

2

 = 2

3

 + 2

1

 + 2

0

 + 2

-1

 = 11,5

10

Zastosowania

Zapis stałoprzecinkowy ma tę zaletę, że arytmetyka stałoprzecinkowa może zostać 
zrealizowana za pomocą działań całkowitoliczbowych. Dzięki temu działania na ułamkach są 
do realizowania tam, gdzie nie ma możliwości użycia liczb zmiennoprzecinkowych: na 
procesorach bez jednostki zmienoprzecinkowej, na prostych mikrokomputerach lub w 

background image

programach używających rozkazów MMX. Zapis stałoprzecinkowy był także powszechnie 
stosowany gdy jednostka zmiennoprzecinkowa procesora była nie dość wydajna, a 
jednocześnie nie była potrzebna wysoka dokładność obliczeń, np. w szybkich procedurach 
graficznych.

Operacje na liczbach stałoprzecinkowych

Jeśli obliczyć wartość liczby stałoprzecinkowej x w naturalnym kodzie dwójkowym, wartość 
ta wyniesie x2

n

. Wówczas działania całkowitoliczbowe mają postać:

Dodawanie/odejmowanie: 

- wynik nie wymaga korekty, 

jest to zapis stałoprzecinkowy z założoną dokładnością.

Mnożenie: a2

n

b2

n

 = ab2

2n

 - wynik wymaga korekty, należy podzielić go przez 2

n

, aby 

uzyskać postać x2

n

.

Dzielenie całkowitoliczbowe. W tym przypadku dzielną a należy przemnożyć przez 
czynnik 2

n

 przed wykonaniem dzielenia i wówczas: a2

2n

 / b2

n

 = (a / b)2

n

.

Mnożenie i dzielenie przez potęgę dwójki, w tym przypadku 2

n

, jest równoważne 

przesunięciu bitowemu (odpowiednio) w lewo bądź prawo o n bitów; jest to operacja bardzo 
szybka.

Zapis zmiennoprzecinkowy

Liczba zmiennoprzecinkowa – komputerowa reprezentacja liczb rzeczywistych zapisanych 
w postaci wykładniczej (zwanej też notacją naukową). Ze względu na wygodę operowania na 
takich liczbach przyjmuje się ograniczony zakres na mantysę i cechę. Powoduje to, że 
reprezentacja jest tylko przybliżona a jedna liczba zmiennoprzecinkowa może reprezentować 
różne liczby rzeczywiste z pewnego odcinka.

Wartość liczby zmiennoprzecinkowej jest obliczana wg wzoru:

gdzie:

S (sign) – znak liczby, 1 lub -1

M (mantissa) – znormalizowana mantysa, liczba ułamkowa

B (base) – podstawa systemu liczbowego (2 dla systemów komputerowych)

E (exponent) – wykładnik, liczba całkowita

Mantysa jest znormalizowana, tj. należy do przedziału [1,B) (przedział prawostronnie 
otwarty!). Jeżeli M jest stałe, a E zmienia się, wówczas przesunięciu ulega przecinek – stąd 
właśnie pochodzi nazwa tej reprezentacji.

Zarówno dla mantysy jak i wykładnika ilość cyfr jest z góry ustalona. Zatem dana liczba jest 
reprezentowana z pewną skończoną dokładnością i należy do skończonego zbioru wartości.

background image

Załóżmy, że m to liczba cyfr przeznaczonych na mantysę, natomiast n+1 to liczba cyfr 
przeznaczonych na wykładnik (n cyfr dla wartości i 1 dla znaku wykładnika). Uznaje się 
również, że jedna dodatkowa pozycja (najstarsza) zarezerwowana jest dla zapisu znaku całej 
liczby. Wówczas wartości maksymalne i minimalne dla M i E określone są następująco:

Wykładnik E

o

E

min

 = − B

n

 + 1

o

E

max

 = B

n

 − 1

Mantysa M

o

M

min

 = 1

o

M

max

 = B − B 

− (m − 1)

Stąd najmniejsza i największa liczba dodatnia możliwa do zapisania w takiej reprezentacji to:

.

Zakres liczb, które mogą być reprezentowane w danym zapisie wynosi:

Zero jest wartością specjalną, która nie może zostać bezpośrednio reprezentowana w tym 
formacie.

Błąd względny reprezentacji wynosi 

(inaczej: waga najmniej znaczącej cyfry 

mantysy). Błędów bezwzględnych na ogół się nie podaje.

Jeżeli komputer wygeneruje liczbę 

, to traktowana jest jako niedomiar 

(underflow).

W przypadku, gdy otrzymana liczba 

, to traktowana jest jako 

nadmiar wykładniczy (overflow).

Przykład reprezentacji

Przyjmijmy, że B = 10, liczba cyfr dziesiętnych przeznaczonych na mantysę wynosi 4, 
natomiast na wykładnik 2. Chcemy zapisać wartość 60,89523.

1. Pierwszy etap to normalizacja mantysy, sytuacja przedstawia się następująco: 

. Mantysa M nie należy do zadanego przedziału [1,10), 

zatem należy przesuwać przecinek w lewo do chwili, aż wartość M będzie doń 
należała. Przesuwanie przecinka w lewo wiąże się ze zwiększaniem E.

2. Po normalizacji (przesunięciu przecinka o 1 pozycje w lewo) otrzymujemy:

background image

3. Ostatnim krokiem jest odpowiednie obcięcie (ang. truncate), albo zaokrąglenie (ang. 

round) mantysy do zadanej ilości cyfr. 

o

obcięcie: 

o

zaokrąglenie: 

Przykład dla liczby mniejszej od 1: 0,0000125.

1. Mantysa 0,0000125 nie jest znormalizowana, należy tym razem przesuwać przecinek 

w prawo, co wiąże się ze zmniejszaniem E.

2. Po normalizacji, tj. przesunięciu przecinka o 5 pozycje w prawo, otrzymujemy: 

.

3. Liczba cyfr znaczących jest mniejsza od dostępnej, więc nie potrzebna żadna forma 

zaokrąglania. Liczba ma postać 

.

Operacje na liczbach zmiennoprzecinkowych

Własności arytmetyki zmiennoprzecinkowej

Arytmetyka zmiennoprzecinkowa nie jest łączna. To znaczy: iż dla xy i z mogą zachodzić 
różności:


,

ani rozdzielna czyli może zachodzić różność:

Innymi słowy kolejność wykonywania operacji wpływa na końcowy wynik.

Przy obliczeniach zmiennoprzecinkowych występują też inne problemy:

zaokrąglenia

nieprawidłowe operacje

przepełnienie

niedomiar

Dodawanie i odejmowanie

Załóżmy że chcemy dodać lub odjąć dwie dodatnie liczby zmiennoprzecinkowe: 

oraz 

, przy czym 

. Założenia te można spełnić 

dla dowolnych liczb zmiennoprzecinkowych manipulując ich kolejnością, znakiem wyniku 
oraz rodzajem wykonywanej operacji, wg poniższego schematu: 

background image

Wówczas:

Jeśli liczby mają różne wykładniki, to podczas dodawania mantysa liczby o mniejszym 
wykładniku musi zostać zdenormalizowana – we wzorze jest to przemnożenie M

2

 przez 

czynnik 

. W szczególnym przypadku, jeśli E

1

 − E

2

 jest większe niż m (ilość cyfr 

mantysy), to po tej denormalizacji mantysa będzie miała wartość 0 i liczba o mniejszym 
wykładniku nie wpłynie na wynik dodawania bądź odejmowania.

Odejmowanie liczb zmiennoprzecinkowych o takim samym wykładniku E i niewiele 
różniącej się mantysie, powoduje że wynikowa mantysa jest znacznie zdenormalizowana. 
Renormalizacja w takim wypadku wiąże się z wprowadzeniem sporej ilości nieznaczących 
zer na końcu mantysy. Jest to szczególnie niekorzystne, dlatego zwykle tak projektuje się 
obliczenia aby do tego nie dopuścić.

Mnożenie i dzielenie

Mając dane liczby zmiennoprzecinkowe 

:

54: Iteracja i rekurencja

Iteracja – czynność powtarzania (najczęściej wielokrotnego) tej samej instrukcji (albo wielu 
instrukcji) w pętli. Mianem iteracji określa się także operacje wykonywane wewnątrz takiej 
pętli.

Czynność iteracji przedstawia pętla, zapisana w poniższym pseudokodzie:

 i=0
 dopóki i<10 wykonuj

    i=i+1
 przeskocz do góry

W pierwszej iteracji otrzymamy wartość i=1, w następnej i=2, potem i=3 i tak dalej, a 
iteracją jest powtarzana tu czynność, czyli zwiększanie zmiennej i o jeden (inkrementacja).

Kod w C/C++:

int i=0;

while (i<10) {
  i++;

}

Kod w Pythonie:

background image

i = 0
while i<10:

i += 1

Kod w Javie, za pomocą pętli for:

for(int i=0; i<10; i++){}

Kod w Scheme, funkcyjnym języku programowania:

;; sum : number -> number
;; to sum the first n natural numbers

(define (sum n)
  (if (and (integer? n) (> n 0))

      (let iter ([n n] [i 1])
        (if (= n 1)

            i
            (iter (- n 1) (+ n i))))

      ((assertion-violation 
       'sum "invalid argument" n))))

Rekurencja albo rekursja (recursion) to w logice, programowaniu i w matematyce 
odwoływanie się np. funkcji lub definicji do samej siebie. Przykład zastosowania rekurencji w 
matematyce: definicja ciągu Fibonacciego 

, dla 

Jest ona rekurencyjna, gdyż definiuje funkcję odwołując się w definicji do niej samej.

Każda definicja rekurencyjna potrzebuje przynajmniej jednego przypadku bazowego (nie 
rekurencyjnego), w tym przypadku są to wartości dla 0 i 1. W przeciwnym wypadku nigdy się 
nie zakończy.

Dla przykładu, obliczenie 

wygląda następująco:

Rekurencja w programowaniu:

Rekurencja jest podstawową techniką wykorzystywaną w funkcyjnych językach 
programowania. Należy jednak zachować ostrożność przy używaniu rekurencji w 

background image

rzeczywistych programach. Ryzyko istnieje szczególnie przy przetwarzaniu dużej ilości 
głęboko zagnieżdżonych danych.

Jeśli program nie jest w rzeczywistości rekurencyjny, to rekurencja może dramatycznie 
zwiększyć złożoność obliczeniową. Ponadto rekurencja zawsze zwiększa pamięciowe 
zapotrzebowanie programu (chyba że zostanie użyta możliwa w pewnych przypadkach 
optymalizacja zwana rekursją ogonową), gdyż wymaga ona zapamiętania m.in. adresów 
powrotu, pozwalających programowi "zorientować się" do którego miejsca ma wrócić po 
zakończeniu jednego z wywołań rekurencyjnych. Inną częstą wadą rekurencji jest kompletnie 
niezależne rozwiązywanie podproblemów, tak, że czasem jeden problem jest rozwiązywany 
w kilku miejscach rozwinięcia rekurencji, np. w powyższym przykładzie obliczania 
niepotrzebnie jest dwukrotnie obliczana wartość 

(porównaj: programowanie 

dynamiczne). Takie problemy nie pojawiają się przy drugim z przykładów. Niezaprzeczalną 
zaletą rekurencji jest przejrzystość programów, które z niej korzystają.

Jedną z typowych sytuacji, w których stosuje się rekurencję jest przeszukiwanie danych o 
strukturze nieregularnego drzewa, np. XML. Funkcja, która sprawdza czy w danym obiekcie 
XML istnieje element o określonej zawartości mogłaby wyglądać następująco (tutaj w PHP 
przy użyciu klasy SimpleXML):

    

function find_text($text, $tree) {

 
        // sprawdź zawartość aktualnego elementu
        if ($text == (string)$tree) {
            return true;
        }
 
        // sprawdź wszystkie jego dzieci
        foreach ($tree as $node) {
 
            // tutaj następuje wywołanie rekurencyjne
            if (find_text($text, $node)) {
                return true;
            }
 
        }
 
        // nic nie znaleziono
        return false;}

Inny przykład – funkcja obliczająca silnię:

unsigned int silnia(unsigned int n) 

 {

background image

     if (n <= 1) {
          return 1;

     } else {
          return n * silnia(n-1);

     }
 }

55: Instrukcje warunkowe i pętle

Instrukcja warunkowa jest w inżynierii oprogramowania jednym ze sposobów na 
organizację kodu i kontrolowania programu. Procesor sprawdza czy warunek znajdująca się 
wewnątrz if jest spełniony (ma wartość logiczną 1). Jeżeli się zgadza to program wykonuje 
blok kodu zawarty po instrukcji if. W przeciwnym wypadku opuszcza go i przechodzi do 
wykonywania kodu znajdującego się za nim. Istnieją różne typy instrukcji warunkowych:

If

if (warunek) then
   {instrukcja jeśli warunek został spełniony}

If-then-else

if (warunek) then 
   (instrukcja jeśli warunek został spełniony)

Else
   (instrukcja gdy warunek nie został spełniony)

End If 

Else-if

if warunek then

    {instrukcja jeśli warunek został spełniony}
elsif warunek2 then

    {instrukcja jeśli warunek2 został spełniony}
elsif warunek3 then

    {instrukcja jeśli warunek został spełniony}
else warunek then

    {instrukcja jeśli żaden z powyższych warunków nie został spełniony}
end if;

Instrukcja warunkowa jako operator

(warunek)?(instrukcja jeśli warunek został spełniony):(instrukcja jeśli 
warunek nie został spełniony)

Tego typu operatory występują w językach o składni typowej dla języka C. 

Instrukcja warunkowa w Haskellu

if' :: Bool -> a -> a -> a

 if' True x _ = x
 if' False _ y = y

Arytmetyczna instrukcja warunkowa (na przykładzie Fortrana)

background image

IF (e) label1, label2, label3

Powyższy kod można zapisać w C++ w ten sposób:

if (e < 0) goto label1

if (e == 0) goto label2
if (e > 0) goto label3

Instrukcja warunkowa w SmallTalku:

var := warunek 
    ifTrue: [ 'foo' ]

    ifFalse: [ 'bar' ]

Instrukcje Case i Switch

Instrukcje case i switch pozwalają na zapisanie skomplikowanej sekwencji warunków w 
przystępny sposób. Można je również zapisać za pomocą kombinacji prostych operatorów if, 
then i else.

W Pascalu:

case zmienna of

  'a': instrukcje wykonane gdy zmienna = a;
  'x': instrukcje wykonane gdy zmienna = x;

  'y','z': instrukcje wykonane gdy zmienna = y lub zmienna = x;
  else instrukcje do wykonania gdy zmienna nie przyjmuje żadnych z 

powyższych wartości;
end;

W C/C++:

switch (zmienna) {
  case 'a': {instrukcje wykonane gdy zmienna = a;} break;

  case 'x': {instrukcje wykonane gdy zmienna = x;} break;
  case 'y':

  case 'z': {instrukcje wykonane gdy zmienna = y lub zmienna = x;} break;
  default: {instrukcje do wykonania gdy zmienna nie przyjmuje żadnych z 

powyższych wartości};
;

}

Pętla to jedna z trzech podstawowych konstrukcji programowania strukturalnego (obok 
instrukcji warunkowej i instrukcji wyboru). Umożliwia cykliczne wykonywanie ciągu 
instrukcji określoną liczbę razy, do momentu zajścia pewnych warunków, dla każdego 
elementu kolekcji lub w nieskończoność.

Pętla for:

for (ilośc wykonań) wykonaj (instrukcje).

Przykłady:

background image

Pascal:

var counter: integer;
 

begin
  for counter:=1 to 10 do

    WriteLn('Linia numer ', counter);
end.

C++:

for (int i=0; i<10; ++i)
  std::cout << i << "-ta iteracja." << std::endl;

PHP:

for ($i=0 ; $i<10 ; ++$i)
   echo $i . "-ta iteracja.<br />";

JavaScript:

var i;
 

for (i=0 ; i<10 ; ++i)
   document.write(i + "-ta iteracja.<br />");

Python:

for i in range(1, 10):
    print(i, '. iteracja', sep='')

Pętla while:

while ( warunek ) wykonuj (instrukcje)

C++:

unsigned int counter = 5;
unsigned long factorial = 1;

 
while (counter > 0)

{
  factorial *= counter--;    /* Multiply and decrement */


 

printf("%lu", factorial);

Java:

int counter = 5;

long factorial = 1;
 

while (counter > 1)
{

   factorial *= counter--;
}

background image

JavaScript:

var counter = 5;
var factorial = 1;

 
while ( --counter > 1 )

{
  factorial *= counter;

}
 

document.body.appendChild(document.createTextNode(factorial));

LUA:

counter = 5

factorial = 1
 

while counter > 0 do
  factorial = factorial * counter

  counter = counter - 1
end

 
print(factorial)

Pascal:

program Factorial1;
var

  Counter, Factorial: integer;
begin

  Counter := 5;
  Factorial := 1;

  while Counter > 0 do
  begin

    Factorial := Factorial * Counter;
    Counter := Counter - 1

  end;
  WriteLn(Factorial)

end.

Perl:

$factorial *= $counter-- while $counter > 0;

Pętla do-while:

do (instrukcje) while (warunek)

Tego typu pętla działa bardzo podobnie do klasycznych pętli while z tą różnicą, że warunek 
jest sprawdzany już po wykonaniu instrukcji, toteż mamy gwarancję że zostaną one 
wykonane co najmniej raz.

Java:

unsigned int counter = 5;
unsigned long factorial = 1;

background image

do {
  factorial *= counter--; /* Multiply, then decrement. */

} while (counter > 0);
printf("%lu\n", factorial);

C/C++:

unsigned int counter = 5;
unsigned long factorial = 1;

do {
  factorial *= counter--; /* Multiply, then decrement. */

} while (counter > 0);
printf("%lu\n", factorial);

JavaScript:

var counter = 5;
var factorial = 1;

do {
  factorial *= counter--; /* Multiply, then decrement. */

} while (counter > 0);
document.body.appendChild(document.createTextNode(factorial));

Pascal:

program Factorial;
var

  Counter, Factorial: integer;
begin

  Counter := 5;
  Factorial := 1;

 
  repeat

    Factorial := Factorial * Counter;
    Dec(Counter);

  until Counter = 0;
 

  WriteLn(Factorial);
end.

PHP:

<?php
$counter = 5;

$factorial = 1;
do {

     $factorial *= $counter--;
} while ($counter > 0);

echo $factorial;
?>

Foreach

foreach (obiekt): (wykonaj instrukcje)

background image

Pętle foreach wykonują instrukcję dla każdego obiektu z tablicy która została przekazana jako 
argument.

Przykłady: 

C/C++:

Te języki nie posiadają odpowiednika pętli foreach.

C#:

foreach (type item in set)
{

  // do something to item
}

Delphi:

for enumerator in collection do

begin
  //do something here

end;

Java:

for (type item: set) {

  // do something to item
}

JavaScript:

for (var strProperty in objObject) {
  if(objObject.hasOwnProperty(strProperty )) {

    /*
      do something to:

      objObject [strProperty]
    */

  }
}

PHP:

foreach ($set as $key => $value) {
  echo "{$key} has a value of {$value}";

}


Document Outline