• Nie Znaleziono Wyników

Wstęp do informatyki

N/A
N/A
Protected

Academic year: 2021

Share "Wstęp do informatyki"

Copied!
88
0
0

Pełen tekst

(1)

Wstęp do informatyki

Marcin Orchel

(2)

Spis treści

1 Podstawy programowania 5

1.1 Wprowadzenie do algorytmiki, instrukcja warunkowa . . . . 5

1.1.1 Wstęp teoretyczny . . . . 5

1.1.2 Zadania . . . . 10

1.2 Algorytm Euklidesa, instrukcje sterujące . . . . 10

1.2.1 Wstęp teoretyczny . . . . 10

1.2.2 Algorytm Euklidesa . . . . 13

1.2.3 Zadania . . . . 14

1.3 Reprezentacja liczba stało i zmienno-przecinkowych w komputerze . . . . 14

1.3.1 Wstęp teoretyczny . . . . 14

1.3.2 Zadania . . . . 17

1.4 Operatory . . . . 18

1.4.1 Wstęp teoretyczny . . . . 18

1.4.2 Zadania . . . . 22

1.5 Funkcje . . . . 22

1.5.1 Wstęp teoretyczny . . . . 22

1.5.2 Zadania . . . . 25

1.6 Tablice . . . . 26

1.6.1 Wstęp teoretyczny . . . . 26

1.6.2 Zadania . . . . 28

1.7 Wyszukiwanie i sortowanie . . . . 29

1.7.1 Wstęp teoretyczny . . . . 29

1.7.2 Algorytmy wyszukiwania . . . . 29

1.7.3 Algorytmy sortowania . . . . 30

1.7.4 Zadania . . . . 33

1.8 Złożoność obliczeniowa . . . . 34

1.8.1 Wstęp teoretyczny . . . . 34

1.8.2 Zadania . . . . 36

1.9 Złożoność obliczeniowa, cd . . . . 36

1.9.1 Wstęp teoretyczny . . . . 36

1.9.2 Zadania . . . . 38

1.10 Wskaźniki . . . . 39

1.10.1 Wstęp teoretyczny . . . . 39

(3)

1.10.2 Zadania . . . . 41

1.11 Wskaźniki, cd. . . . . 42

1.11.1 Wstęp teoretyczny . . . . 42

1.11.2 Zadania . . . . 43

1.12 Pliki tekstowe. Przeładowanie nazw funkcji . . . . 44

1.12.1 Wstęp teoretyczny . . . . 44

1.12.2 Zadania . . . . 45

1.13 Różnice między językami C i C++ . . . . 46

1.13.1 Wybrane konstrukcje poprawne w C, a niepoprawne w C++. . . . 46

1.13.2 Konstrukcje poprawne w C i C++, ale zachowujące się inaczej . . 46

1.13.3 Konstrukcje poprawne w C++, a niepoprawne w C . . . . 47

2 Zaawansowane programowanie 49 2.1 Programowanie obiektowe . . . . 49

2.1.1 Paradygmaty programowania . . . . 49

2.1.2 Fundamentalne pojęcia programowania obiektowego . . . . 49

2.1.3 Zadanie lekcyjne . . . . 50

2.1.4 Zadania domowe . . . . 50

2.2 Programowanie obiektowe, cd. . . . . 51

2.2.1 Elementy programowania obiektowego w C++ . . . . 51

2.2.2 Operacje na obiektach . . . . 52

2.2.3 Dziedziczenie . . . . 52

2.2.4 Zadania domowe . . . . 52

2.3 Programowanie obiektowe, cd. . . . . 53

2.3.1 Operacje na obiektach . . . . 53

2.3.2 Dziedziczenie . . . . 53

2.3.3 Zadanie lekcyjne . . . . 53

2.3.4 Zadanie domowe . . . . 53

2.4 Programowanie obiektowe, cd. . . . . 54

2.4.1 Elementy programowania obiektowego w C++ . . . . 54

2.4.2 Zadanie domowe . . . . 54

2.5 Programowanie obiektowe, cd. . . . . 54

2.5.1 Zadanie domowe . . . . 54

2.6 Wzorce w C++ . . . . 54

2.6.1 Wstęp . . . . 54

2.6.2 Wzorce funkcji . . . . 54

2.6.3 Wzorce klasy . . . . 55

2.6.4 Wzorzec projektowy strategii . . . . 56

2.6.5 Zadanie domowe . . . . 62

2.7 wzorce w C++ cd. . . . . 62

2.7.1 Wstęp teoretyczny . . . . 62

2.7.2 Zadanie domowe . . . . 63

2.8 Biblioteka standardowa C++ . . . . 63

(4)

2.8.1 Biblioteka standardowa . . . . 63

2.8.2 Wektor . . . . 65

2.9 Biblioteka standardowa C++ . . . . 65

2.9.1 list . . . . 65

2.9.2 deque . . . . 65

2.9.3 stack . . . . 65

2.9.4 queue . . . . 65

2.9.5 priority_queue . . . . 65

2.10 Biblioteka standardowa C++ . . . . 65

2.10.1 set . . . . 65

2.10.2 multiset . . . . 65

2.10.3 map . . . . 65

2.10.4 multimap . . . . 65

2.10.5 hash_set, hash_multiset, hash_map, hash_multimap . . . . 65

2.10.6 bitset . . . . 65

2.10.7 valarray . . . . 65

2.11 Biblioteka standardowa C++ . . . . 65

2.11.1 binary_search . . . . 65

2.11.2 fill, fill_n . . . . 65

2.11.3 find, find_first_of . . . . 65

2.11.4 find_end, find_if . . . . 65

2.11.5 for_each . . . . 65

2.11.6 make_heap . . . . 65

2.11.7 remove . . . . 65

2.11.8 reverse . . . . 65

2.12 Biblioteka standardowa C++ . . . . 65

2.12.1 search . . . . 65

2.12.2 sort . . . . 65

2.12.3 sort_heap . . . . 65

2.12.4 partial_sort . . . . 65

2.12.5 transform . . . . 65

2.12.6 lexicographical comparison . . . . 65

2.13 Biblioteka standardowa C++ . . . . 65

2.13.1 functors . . . . 65

2.13.2 complex . . . . 65

2.13.3 memory . . . . 65

2.13.4 IOstream . . . . 65

2.13.5 Biblioteka standardowa C. Różnice między wersją z C i przenie- sioną do C++ . . . . 65

2.13.6 string . . . . 65

2.13.7 Standard C++0x . . . . 65

(5)

3 Język SQL 66

3.1 Relacyjne bazy danych wprowadzenie . . . . 66

3.1.1 Relacyjne bazy danych projektowanie . . . . 66

3.1.2 Zadania . . . . 66

3.1.3 Laboratoria . . . . 66

3.2 SQL, podstawowe zapytania . . . . 66

3.2.1 SELECT . . . . 67

3.2.2 INSERT . . . . 67

3.2.3 UPDATE . . . . 67

3.2.4 DELETE . . . . 68

3.2.5 Zadania . . . . 68

3.3 SQL, GROUP BY, HAVING . . . . 68

3.3.1 GROUP BY . . . . 68

3.3.2 HAVING . . . . 69

3.3.3 Zadania . . . . 70

3.4 SQL, JOIN . . . . 70

3.4.1 INNER JOIN . . . . 70

3.4.2 LEFT JOIN . . . . 71

3.4.3 RIGHT JOIN . . . . 71

3.4.4 FULL OUTER JOIN . . . . 71

3.4.5 Self join . . . . 71

3.4.6 Zadania . . . . 72

3.5 SQL, podzapytania . . . . 72

3.5.1 Zadania . . . . 74

3.6 SQL, LIKE, IN, CASE, EXISTS . . . . 75

3.6.1 LIKE . . . . 75

3.6.2 BETWEEN . . . . 76

3.6.3 IN . . . . 76

3.6.4 EXISTS . . . . 76

3.6.5 WYRAŻENIA CASE . . . . 77

3.6.6 Zadania . . . . 78

3.7 SQL, UNION . . . . 78

3.7.1 UNION . . . . 78

3.7.2 INTERSECT, EXCEPT . . . . 79

4 Statystyka w SQL 81 4.1 Podstawowe statystyki . . . . 81

4.1.1 Zadania . . . . 82

4.2 Wariancja . . . . 82

4.2.1 Zadania . . . . 84

4.3 Kowariancja . . . . 85

4.3.1 Zadania . . . . 86

(6)

Rozdział 1

Podstawy programowania

1.1 Wprowadzenie do algorytmiki, instrukcja warunkowa

1.1.1 Wstęp teoretyczny Standardy C

• ANSI C, inaczej Standard C, inaczej C89, inaczej ANSI X3.159-1989.

• ISO/IEC 9899: 1990, inaczej C90 (praktycznie to samo co C89)

• IOS/IEC 9899: 1999, inaczej C99 Standardy C++

• ISO/IEC 14882:1998

• 2003 poprawiona wersja standardu C++

• 2005 technical report: Library Technical Report 1 (TR1) Przykład z sumowaniem liczb

Zapis w języku C++.

1 /∗ program sumuj ą cy d w i e l i c z b y ∗/

2 void sum ( ) // d e f i n i c j a f u n k c j i 3 {

4 i n t l i c z b a 1 = 2 0 ; // d e f i n i c j a z m i e n n e j 5 i n t l i c z b a 2 = 3 0 ; // d e f i n i c j a z m i e n n e j

6 i n t suma = l i c z b a 1 + l i c z b a 2 ; // d e f i n i c j a z m i e n n e j suma 7 p r i n t f ( " suma␣ l i c z b : ␣%d " , suma ) ; // w y p i s a n i e sumy

8 }

Zapis w pseudojęzyku:

Na rysunku Rys. 1.1 pokazany jest schemat blokowy dla algorytmu sumowania liczb.

(7)

Alg. 1 Suma dwóch liczb

Warunki początkowe: liczba1 ∈ Z, liczba2 ∈ Z Warunki końcowe: sum = liczba1 + liczba2

1: sum ⇐ liczba1 + liczba2

2: Wypisz: sum

Rysunek 1.1: Schemat blokowy dla algorytmu sumowania.

Język programowania C++

Algorytmika

Pseudojęzyk Elementy pseudojęzyka:

• Nazwa algorytmu

• Opis algorytmu

• Warunki początkowe

• Warunki końcowe

• instrukcje specjalne, jak np. instrukcja warunkowa

• inne instrukcje

Schemat blokowy Elementy schematu blokowego:

• początek algorytmu

• koniec algorytmu

• strzałki

• blok operacyjny

• bloki wejścia/wyjścia

• blok decyzyjny

(8)

Alg. 2 Instrukcja warunkowa

1: if warunek then

2: {instrukcje jesli warunek jest spelniony}

3: else

4: {instrukcje jesli warunek jest spelniony}

5: end if

Instrukcja warunkowa

Instrukcja warunkowa pozwala na wykonanie odpowiedniej listy instrukcji w zależności od wartości warunku logicznego. W języku C++ wygląda następująco:

1 i f ( warunek )

2 {

3 // l i s t a i n s t r u k c j i , k t o r e z o s t a n a wykonane 4 // j e s l i warunek j e s t p r a w d z i w y

5 } e l s e

6 {

7 // l i s t a i n s t r u k c j i , k t o r e z o s t a n a wykonane 8 // j e s l i warunek n i e j e s t p r a w d z i w y

9 }

Zapis instrukcji warunkowej w pseudojęzyku został przedstawiony w Alg. 2, zaś w sche- macie blokowym na Rys. 1.2.

Rysunek 1.2: Schemat blokowy z instrukcją warunkową.

Istnieje możliwość łączenia kilku instrukcji warunkowych, w języku C++ wygląda to następująco:

1 i f ( warunek1 )

2 {

3 // l i s t a i n s t r u k c j i , k t o r e z o s t a n a wykonane

4 // j e s l i warunek j e s t p r a w d z i w y

(9)

Alg. 3 Instrukcja warunkowa

1: if warunek then

2: {lista instrukcji, ktore zostana wykonane, jesli warunek jest prawdziwy}

3: else if warunek2 then

4: {lista instrukcji, ktore zostana wykonane, jesli warunek1 nie jest prawdziwy

oraz warunek2 jest prawdziwy}

5: else

6: {lista instrukcji, ktore zostana wykonane, jesli zaden z warunkow nie jest prawdziwy}

7: end if

5 } e l s e i f ( warunek2 )

6 {

7 // l i s t a i n s t r u k c j i , k t o r e z o s t a n a wykonane 8 // j e s l i warunek1 n i e j e s t p r a w d z i w y

9 // o r a z warunek2 j e s t p r a w d z i w y 10 } e l s e

11 {

12 // l i s t a i n s t r u k c j i , k t o r e z o s t a n a wykonane 13 // j e s l i z a d e n z warunkow n i e j e s t p r a w d z i w y

14 }

W pseudojęzyku łączenie instrukcji warunkowych przedstawione jest w Alg. 3, zaś w schemacie blokowym na Rys. 1.3.

Rysunek 1.3: Schemat blokowy z dwoma połączonymi instrukcjami warunkowymi.

(10)

Przykład z nierównością trójkąta Zapis w języku C++.

1 /∗ program s p r a w d z a j ą cy warunek t r ó j k ą t a ∗/

2 void w a r u n e k T r o j k a t a ( ) 3 {

4 i n t bokA = 4 ; // d e f i n i c j a z m i e n n e j 5 i n t bokB = 3 ; // d e f i n i c j a z m i e n n e j 6 i n t bokC = 6 ; // d e f i n i c j a z m i e n n e j 7 i f ( bokA >= bokB + bokC

8 | | bokB >= bokA + bokC

9 | | bokC >= bokA + bokB ) // warunek t r o j k a t a

10 {

11 p r i n t f ( warunek t r ó j k ą t a n i e s p e ł n i o n y " ) ; 12 ␣ ␣ ␣ ␣ }

13 ␣ ␣ ␣ ␣ e l s e 14 ␣ ␣ ␣ ␣ {

15 ␣ ␣ ␣ ␣ ␣ ␣ ␣ ␣ p r i n t f ( " warunek t r ó j k ą t a s p e ł n i o n y " ) ; 16 ␣ ␣ ␣ ␣ }

17 }

Jeśli wiadomo, że jeden z boków jest większy lub równy od dwóch pozostałych to sprawdzanie warunku trójkąta sprowadza się do sprawdzenia zachodzenia jednej nierów- ności: jeśli założymy, że bokA >= bokB i bokA >= bokC to wystarczy sprawdzenie warunku bokA >= bokB + bokC.

Techniki programowania

• umieszczanie zawsze w instrukcji warunkowej bloku else

• w niektórych przypadkach łączenie dwóch instrukcji warunkowych w jedną za po- mocą operatora koniunkcji, przykładowo w języku C++ kod postaci:

1 i f ( warunek1 )

2 {

3 i f ( warunek2 )

4 {

5 i n s t r u k c j a 1 ; 6 } e l s e

7 {

8 // empty

9 }

10 } e l s e

11 {

12 // empty

13 }

(11)

możemy zapisać w postaci:

1 i f ( warunek1 && warunek2 )

2 {

3 i n s t r u k c j a 1 ; 4 } e l s e

5 {

6 // empty

7 }

1.1.2 Zadania Zadania na 3.0

Napisać algorytm sprawdzający położenie punktu A (x A , y A ) względem danej prostej y = ax + b

Algorytm zapisać w pseudojęzyku i schemacie blokowym.

Zadania na 4.0

Zapisać algorytm wyliczania rozwiązania układu dwóch równań liniowych z dwoma zmiennymi za pomocą pseudojęzyka i schematu blokowego.

Zadania na 5.0

Mamy dany odcinek AB w kartezjańskim układzie współrzędnych określony przez dwa punkty A (x A , y A ) i B (x B , y B ) oraz punkt C (x C , y C ). Napisać algorytm w pseudoję- zyku, który sprawdza czy punkt C należy do odcinka AB.

1.2 Algorytm Euklidesa, instrukcje sterujące

1.2.1 Wstęp teoretyczny Pętla while

Przykład w C++:

1 i n t i = 2 0 ; 2 while ( i > 1 0 )

3 {

4 p r i n t f ( " l i c z b a ␣ i : ␣%d\n " , i ) ;

5 i −−;

6 }

Zapis algorytmiczny:

(12)

Alg. 4 Pętla while

1: while warunek do

2: {instrukcje}

3: end while

Alg. 5 Pętla repeat − until

1: repeat

2: {instrukcje}

3: until warunek

Pętla do − while Przykład w C++:

1 i n t i = 2 0 ;

2 do

3 {

4 p r i n t f ( " l i c z b a ␣ i : ␣%d\n " , i ) ;

5 i −−;

6 } while ( i > 1 0 ) ; Zapis algorytmiczny:

Pętla f or

Przykład w C++:

1 f o r ( i n t i = 2 0 ; i > 1 0 ; i −−)

2 {

3 p r i n t f ( " l i c z b a ␣ i : ␣%d\n " , i ) ;

4 }

Pętla f or może być zapisana za pomocą pętli while.

Pętla nieskończona Język C++:

1 while ( true )

2 {

3 p r i n t f ( " p e t l a " ) ;

4 }

Zapis algorytmiczny:

Instrukcja break

(13)

Alg. 6 Pętla nieskończona

1: loop

2: {instrukcje}

3: end loop

1 f o r ( i n t i = 2 0 ; i > 1 0 ; i −−)

2 {

3 i f ( i == 1 5 )

4 {

5 break ;

6 }

7 p r i n t f ( " l i c z b a ␣ i : ␣%d\n " , i ) ;

8 }

Pętle z użyciem instrukcji break można zapisać równoważnie bez tej instrukcji w nastę- pujący sposób:

1 bool done = f a l s e ;

2 f o r ( i n t i = 2 0 ; i > 10 && ! done ; i −−)

3 {

4 i f ( i == 1 5 )

5 {

6 done = true ;

7 } e l s e

8 {

9 p r i n t f ( " l i c z b a ␣ i : ␣%d\n " , i ) ;

10 }

11 }

Instrukcja continue

1 f o r ( i n t i = 2 0 ; i > 1 0 ; i −−)

2 {

3 i f ( i == 1 5 )

4 {

5 // i n n e i n s t r u k c j e

6 continue ;

7 }

8 p r i n t f ( " l i c z b a ␣ i : ␣%d\n " , i ) ;

9 }

Pętle z instrukcją continue można zapisać bez tej instrukcji następująco:

1 f o r ( i n t i = 2 0 ; i > 1 0 ; i −−)

2 {

3 i f ( i == 1 5 )

(14)

4 {

5 // i n n e i n s t r u k c j e 6 } e l s e

7 {

8 p r i n t f ( " l i c z b a ␣ i : ␣%d\n " , i ) ;

9 }

10 }

1.2.2 Algorytm Euklidesa

Algorytm Euklidesa służy do obliczenia największego wspólnego dzielnika dwóch liczb (oznaczenie: NWD(a,b) lub gcd(a, b)). Algorytm podstawowy obliczania gcd(a, b) polega na znalezieniu wszystkich dzielników liczby a, później b, a następnie wybraniu najwięk- szego wspólnego dzielnika. Algorytm Euklidesa opiera się na własności, że:

gcd (a, b) = gcd (a − b, b) dla a > b oraz

gcd (a, b) = gcd (a, b − a) dla b > a. Jeśli a = b to gcd (a, b) = a.

Istnieje również druga wersja algorytmu Euklidesa, gdzie korzystamy z własności:

gcd (a, b) = gcd (a mod b, b − (a mod b))

Implementacja obu wersji dla a, b > 0 została przedstawiona poniżej:

1 i n t tempNumber1 = a ; 2 i n t tempNumber2 = b ;

3 while ( tempNumber1 != tempNumber2 )

4 {

5 i f ( tempNumber1 > tempNumber2 )

6 {

7 tempNumber1 = tempNumber1 − tempNumber2 ; 8 } e l s e

9 {

10 tempNumber2 = tempNumber2 − tempNumber1 ;

11 }

12 }

13 p r i n t f ( "NWD␣=␣%d\n " , tempNumber1 ) ; 1 i n t tempNumber1 = a ;

2 i n t tempNumber2 = b ; 3 while ( tempNumber1 > 0 )

4 {

5 tempNumber1 = tempNumber1 % tempNumber2 ;

6 tempNumber2 = tempNumber2 − tempNumber1 ;

(15)

7 }

8 p r i n t f ( "NWD␣=␣%d\n " , tempNumber2 ) ;

Można pokazać, że oba powyższe algorytmy zawsze zakończą swoje działanie.

1.2.3 Zadania Zadania na 3.0

Sprawdzić, czy podana liczba jest liczbą pierwszą.

Zadania na 4.0

Sprawdzić, czy podana liczba jest liczbą doskonałą. Liczba doskonała to liczba na- turalna, która jest sumą swoich dzielników właściwych. Przykład liczby doskonałej:

28 = 2 · 2 · 7 = 1 + 2 + 7 + 4 + 14.

Zadania na 5.0

Wyznaczyć metodą iteracyjną kolejne wartości liczb Fibonacciego.

1.3 Reprezentacja liczba stało i zmienno-przecinkowych w komputerze

1.3.1 Wstęp teoretyczny

Specyfikacja możliwych typów w języku C znajduje się w Tablica 1.1. To czy typ int (lub char) jest typem signed int (signed char) czy unsigned int (unsigned char) jest zależne od implementacji.

Zakres liczb całkowitych 1 bajtowych: 0 - 255.

Zakres liczb całkowitych 2 bajtowych: 0 - 65535.

Zakres liczb całkowitych 4 bajtowych: 0 - 4294967295.

Zakres liczb całkowitych 1 bajtowych ze znakiem: -128 - 127.

Zakres liczb całkowitych 2 bajtowych ze znakiem: -32768 - 32767.

Zakres liczb całkowitych 4 bajtowych ze znakiem: -2147483648 - 2147483647.

Stałe. Stałe znakowe. Liczby całkowite. Stałe typu long zapisujemy z l na końcu.

Stałą typu unsigned zapisujemy z literką u na końcu. Zaś stałą typu unsigned long z literkami ul. Liczby całkowite można przedstawiać w postaci ósemkowej za pomocą 0 na początku, lub w postaci szesnastkowej za pomocą 0x na początku. Stałe zmiennopo- zycyjne zapisujemy z kropką dziesiętną lub w notacji naukowej, jeśli na końcu jest L to są typu long double. Stała zmiennopozycyjna jest domyślnie typu double, z literką f na końcu jest typu f loat, a z literką l jest typu long double.

W języku C występuje również typ wyliczeniowy deklarowany za pomocą słowa klu-

czowego enum.

(16)

Deklaracje zmiennych mogą być poprzedzone słowem kluczowym języka C const.

Liczby całkowite

Liczby całkowite są zapisane w komputerze w kodzie uzupełnień do dwóch (U2). Liczby dodatnie są reprezentowane binarnie w standardowy sposób, i dodawany jest bit 0 na początku tej liczby. Liczby przeciwne powstają przez odjęcie liczby od dwukrotnej wagi najstarszego bitu 2 · 2 n−1 = 2 n . W kodzie uzupełnień do jednego (U1) liczby przeciwne powstają przez odjęcie liczby od liczby składającej się z samych jedynek, np −1 = 11111110. Przykład dla dwóch bitów znalezienie liczby przeciwnej do 1 w u2:

1 10 = 01 2

−1 = 100 2 − 01 2 = 11 2

Operacja wyszukiwania liczby przeciwnej może być przeprowadzona również w systemie dziesiętnym: Przykład: znaleźć reprezentację 4-bitową liczby -5.

−5 10 = 2 4 − 5 10 = 11 10 = 1011 2 Na n-bitach można zapisać liczby z zakresu:

h −2 n−1 , 2 n−1 − 1 i Przykład zapisu liczb na 4 bitach:

0111, 7 0110, 6 0101, 5 0100, 4 0011, 3 0010, 2 0001, 1 0000, 0 1111, -1 1110, -2 1101, -3 1100, -4 1011, -5 1010, -6 1001, -7 1000, -8

W porównaniu do zapisu znak-moduł kolejność zapisu liczb ujemnych jest odwrócona

i są przesunięte o 1, tak że nie ma podwójnego zera, co oznacza, że zero jest zapisane

tylko za pomocą jednego kodu. Operacje dodawania i odejmowania są wykonywane tak

samo jak dla liczb binarnych bez znaku. W metodzie tej liczby ujemne mają pierwszy

bit równy 1.

(17)

Sposób konwersji liczby w kodzie u2 do systemu dziesiętnego w kodzie znak-moduł polega na wstawieniu znaku minus przy najwyższej potędzie 2:

1101 u2 = −2 3 + 2 2 + 2 0 = −3 zm

Alternatywna metoda zamiany na liczbę przeciwną w kodzie u2: dokonujemy inwersji bitów, a następnie wynik zwiększamy o 1.

Przykład:

−5 = − (0101 u2 ) = 1010 2 + 1 2 = 1011 u2 = 5

Dodawanie pisemne liczb całkowitych w systemie u2. Przykład dla liczb 8 bitowych:

15 zm = 00001111 u2

−5 zm = 11111011 u2 15 + (- 5)

11111111 00001111 +11111011

− − − − − 00001010

Ostatnie przeniesienie do 9 bitu jest ignorowane.

Liczby rzeczywiste

Konwersja liczby 0,1 do postaci binarnej:

0, 1 = 2 0 · 0 + 0, 1 2 −1 = 0, 5

0, 1 = 2 0 · 0 + 2 −1 · 0 + 0, 1 2 −2 = 0, 25

0, 1 = 2 0 · 0 + 2 −1 · 0 + 2 −2 · 0 + 0, 1 2 −3 = 0, 125

0, 1 = 2 0 · 0 + 2 −1 · 0 + 2 −2 · 0 + 2 −3 · 0 + 0, 1 2 −4 = 0, 0625

0, 1 = 2 0 · 0 + 2 −1 · 0 + 2 −2 · 0 + 2 −3 · 0 + 2 −4 · 1 + 0, 0375 0, 1 10 = 0, 0001 2

. . .

(18)

Skończona reprezentacja liczb dziesiętnych w systemie binarnym. Przykład:

3

8 = 0, 375

0, 375 = 2 0 · 0 + 2 −1 · 0 + 2 −2 · 1 + 2 −3 · 1

Liczby dziesiętne, które można przedstawić w postaci sumy dowolnych potęg dwójki mają skończoną reprezentację binarną.

Znormalizowana reprezentacja liczb zmiennoprzecinkowych w komputerze:

x = ±0, b 1 b 2 . . . b n · 2 E

gdzie b 1 6= 0; 0, b 1 b 2 . . . b n - mantysa; E - liczba całkowita zwana cechą.

Przykład:

0, 0001 10 = 0, 1 · 2 −3

Sumowanie liczb Założenia mantysa 5 pozycji, cecha 1 pozycja. Przykład:

a = 0, 1234 · 10 3 = 123, 4 b = 0, 2345 · 10 2 = 23, 45

a + b = 0, 1234 · 10 3 + 0, 02345 · 10 3 = 0, 14685 · 10 3 = 146, 85 a + b = 0, 1234 · 10 3 + 0, 0234 · 10 3 = 0, 1468 · 10 3 = 146, 8 Błąd bezwzględny:

146, 85 − 146, 8 = 0, 05 Błąd względny:

0, 05

146, 85 · 100% = 0, 034%

1.3.2 Zadania Zadania na 3.0

Wykonać dodawanie przy pomocy znormalizowanej reprezentacji liczb zmiennoprzecin- kowych następujących liczb:

123456.7 101.7654

dla reprezentacji z 1 pozycją na cechę i z 6 pozycjami na mantysę. Podać wartość błędu bezwzględnego i względnego.

Zadania na 4.0

Po zamianie podanych liczb w systemie u2 do precyzji 8 bitowej również w systemie u2

pomnożyć je: 110 u2 i 011 u2 .

(19)

Zadania na 5.0

Przeanalizować, w którym wyrażeniu

x 2 + y 2 (x − y) (x + y)

błędy zaokrągleń są mniejsze, podać co najmniej 3 różne przykłady potwierdzające wnio- ski.

1.4 Operatory

1.4.1 Wstęp teoretyczny

• Dwuargumentowe operatory arytmetyczne: +, -, *, /, %.

• Operatory relacji: <, >=, <, <=.

• Operatory przyrównania: ==, !=.

• Operatory logiczne: &&, ||. Wyrażenia z tymi operatorami obliczamy od lewej do prawej, jeśli w trakcie analizy kolejnych operandów jest wiadome, jaką wartość będzie miało całe wyrażenie to kolejne operandy nie są obliczane.

• Jednoargumentowy operator negacji !.

• operatory zwiększania i zmniejszania: ++, - - mogą być przedrostkowe jak i przy- rostkowe. W przypadku przedrostkowym wartość wyrażenia zmienia się przed jego użyciem, a w przypadku przyrostkowym wartość wyrażenia zmienia się po jego użyciu.

i n t a1 = 1 ; i n t a2 = 1 ; i n t l 1 = ++a1 ; i n t l 2 = a2++;

Wynikiem jest l1 = 2, l2 = 1.

Wyrażenia logiczne w C mają wartość niezerową, jeśli są prawdziwe i 0, jeśli są fałszywe.

Konwersja typów. Automatyczne przekształcenie argumentu „ciaśniejszego” na

„obszerniejszy” bez utraty informacji. Przykład:

i n t i = 2 + 0 . 6 + 0 . 6 ;

Liczba całkowita 2 zostanie przekształcona na liczbę typu double 2.0. Zmienna i będzie

miała wartość 3. Możliwa jest również konwersja stratna np. w przypadku przypisania

wyrażenia zmiennopozycyjnego do zmiennej całkowitej:

(20)

i n t i = 0 . 3 ;

W przypadku dzielenia liczby całkowitej przez całkowitą należy pamiętać, aby dzielna była liczbą zmiennopozycyjną lub dzielnik.

double d = 3 / 4 ;

Zmienna d będzie miała wartość 0.0. Jednak zazwyczaj chodzi nam o otrzymanie war- tości 0,75:

double d = 3 . 0 / 4 ;

Dobrym zwyczajem jest zapisywanie stałych w docelowych typach, ze względu na czy- telność programu, a więc w powyższym przypadku możemy zapisać:

double d = 3 . 0 / 4 . 0 ;

Operator rzutowania. (nazwa typu) wyrażenie. W przypadku rzutowania zmiennej całkowitej 2 bajtowej int do 1 bajtowej char, traci się najbardziej znaczący bajt (ten od lewej). Przykład: (unsigned char)10000001 = 1.

Operatory przypisania =, +=, -=, *=, /=, %=, <<=, >>=, &=, |=, ^= . Przypi- sanie ma wartość o typie lewego argumentu i wartości przypisanej.

Wyrażenia warunkowe: wyr1 ? wyr2 : wyr3.

Operatory bitowe.

• & - bitowa koniunkcja (AND). Wartości 1 i 0 traktowane są jako wartości logiczne.

Dokonywana jest koniunkcja na każdym bicie. Służy do zerowania poszczególnych bitów w danej zmiennej. Jeden z argumentów można rozumieć jako maskę, po- nieważ dla tych bitów, które ustawione są w nim na 1 wartości bitów pierwszego argumentu nie zmieniają się. Przykład: 111101 & 000111 = 000101.

• | - bitowa alternatywa (OR). Dokonywana jest alternatywa na każdym bicie. Służy do ustawiania 1 na poszczególnych bitach. Również tutaj można traktować jeden z argumentów jako maskę, ponieważ dla tych bitów, które ustawione są na 0 war- tości bitów pierwszego argumentu nie zmieniają się. Przykład: 111101 | 000111 = 111111

• ^- bitowa różnica symetryczna (XOR). Dokonywana jest różnica symetryczna na każdym bicie, polegająca na ustawianiu 1 wszędzie gdzie bity w obu argumentach są różne, w przeciwnym razie 0. Wartość w masce 0 nie zmienia bitu, a 1 przełącza bit. Możemy zamienić każdy bit 1 na 0 i odwrotnie za pomocą bitowej różnicy symetrycznej z maską z samymi 1, jakkolwiek dla tej operacji istnieje osobny ope- rator dopełnienia jedynkowego. Bitowa różnica symetryczna ma również tą właści- wość, że zastosowanie dwukrotnie maski przywraca poprzednią wartość zmiennej, dlatego może być z łatwością użyta do kodowania i rozkodowywania. Przykład:

111101^000111 = 111010

111010^000111 = 111101.

(21)

• << - przesunięcie w lewo. Przesunięcie bitów w lewo w argumencie stojącym po lewej stronie operatora o liczbę pozycji określoną przez argument po prawej stronie operatora. Zwolnione bity są wypełniane zerami. Przykład: 111 << 2 = 11100.

Przesunięcie o jedną pozycję powoduje dodanie jednego zera i odpowiada mnożeniu liczby przez 2.

• >> - przesunięcie w prawo. Przesunięcie bitów w prawo w argumencie stojącym po lewej stronie operatora o liczbę pozycji określoną przez argument po prawej stronie operatora. Zwolnione bity są wypełniane zerami, jednakże dla liczb ze znakiem zwolnione bity na niektórych maszynach wypełniane są bitem znaku. Przykład:

00000111 >> 2 = 1, przy wypełnianiu bitem znaku: 11111111 >> 2 = 11111111.

Przesunięcie w prawo o jedną pozycję odpowiada dzieleniu przez 2:

0110 - 6 0011 - 3 0001 - 1.

Dla wypełniania bitem znaku otrzymujemy również poprawne dzielenie przez 2 dla liczb ujemnych:

1010 - -6 1101 - -3 1110 - -2.

• ˜ - dopełnienie jedynkowe (operator jednoargumentowy). Zamienia każdy bit 1 na 0 i odwrotnie. Przykład: wyzerowanie ostatnich 6 bitów w dowolnej liczbie całkowitej. A więc trzeba zrobić bitową koniunkcję z liczbą, która ma same jedynki i sześć ostatnich bitów 0. Gdy chcemy używać taką stałą z liczbami całkowitymi o dowolnej długości możemy ją zapisać jako: ˜111111.

Proste operacje bitowe (najmniej znaczący bit ma indeks 0):

• ustawienie n-tego bitu na 1

unsigned char c |= ( 1 << n )

• ustawienie n-tego bitu na 0

unsigned char c &= ~(1 << n )

• przełączenie n-tego bitu

unsigned char c ^= ( 1 << n )

• testowanie n-tego bitu, czy n-ty bit jest ustawiony na 1, unsigned char e = d & ( 1 << n )

Testowanie to można również zrealizować następująco:

unsigned char e = 1 & ( d >> n )

(22)

Przykłady wykorzystania operacji bitowych.

Przykład 1. Zadanie polega na zwróceniu n bitów wyciętych ze zmiennej x od pozycji p, dosunięte do prawej strony wyniku. Zmienna x jest typu unsigned, wynik jest również typu unsigned. Nie zakładamy nic o rozmiarze tych zmiennych.

return ( x>>(p−n ) ) & ~(~0<<n ) ;

Rozwiązanie składa się z dwóch kroków, dosunięcie pola do prawego końca, a następnie wyzerowanie bitów znajdujących się przed tym polem. Przykład: 11000101, chcemy wyciąć 100, czyli n = 3, p = 7. Zmienna x zostaję najpierw przesunięta o 4 pozycje i otrzymujemy: 00001100. Teraz trzeba wyzerować wszystkie bity poza polem, a więc trzeba stworzyć maskę postaci 00000111, i dokonać bitowej koniunkcji. Maska powstaje przez dopełnienie jedynkowe maski 11111000. Maska ta powstaje za pomocą przesunięcia

˜0 << n.

Przykład 2. Jeśli chcemy przetestować czy na danych bitach znajdują się określone wartości, to możemy to zrobić następująco: najpierw tworzymy maskę, taką, że ma same 0 na bitach, które nie są interesujące, a na pozostałych ma 1. Po zastosowaniu koniunkcji zerujemy nieistotne bity w zmiennej. Następnie możemy zastosować różnicę symetryczną z maską, która ma same zera na nieistotnych bitach i testowe wartości na bitach istotnych. Jeśli wynik jest zerowy oznacza to, że na określonych bitach wartości testowe są te same co w zmiennej.

Przykład 3. Mnożenie przez stałą da się wykonać za pomocą wielu dodawań i prze- sunięć. Przykład: 12 = 2 3 + 2 2

a · 12 = a  2 3 + 2 2  = a2 3 + a2 2

return ( a<<3) + ( a <<2);

Przykład 4. Znalezienie minimum dwóch liczb:

r = y + ( ( x − y ) & −(x < y ) ) ;

Przykład: Gdy pierwsza liczba jest mniejsza od drugiej, to drugi składnik sumy będzie równy modułowi różnicy między tymi liczbami ze znakiem ujemnym, ponieważ drugi człon koniunkcji będzie liczbą składającą się z samych bitowych jedynek. Natomiast gdy pierwsza liczba jest większa od drugiej, to drugi składnik jest 0, ponieważ drugi człon koniunkcji jest równy zero i zeruje pierwszy człon koniunkcji.

Optymalizacje bitowe:

• dzielenie przez potęgi 2 może być zapisane za pomocą przesunięcia,

• reszta z dzielenia przez potęgi dwójki może być zapisana za pomocą koniunkcji

bitowej.

(23)

1.4.2 Zadania Zadania na 3.0

Napisać program zliczający liczbę bitów 1 liczby całkowitej typu unsigned o dowolnym rozmiarze.

Zadania na 4.0

Napisać program bez użycia pętli, z użyciem operatorów bitowych, który dokonuje za- okrąglenia liczby do najbliższej od góry potęgi dwójki. Do programu dołączyć omówienie.

Zadanie dodatkowe nieobowiązkowe: Napisać program bez użycia pętli, z użyciem ope- ratorów bitowych, który dokonuje zaokrąglenia liczby do najbliższej potęgi dwójki.

Zadania na 5.0

Napisać program bez użycia pętli, z użyciem operatorów bitowych, który sprawdza czy liczba jest potęgą dwójki. Do programu dołączyć omówienie.

Zadania na 6.0

Napisz program bez użycia pętli, z użyciem operatorów bitowych zwracający informacje czy w liczbie znajduje się parzysta liczba bitów 1, czy nieparzysta. Do programu dołączyć omówienie.

1.5 Funkcje

1.5.1 Wstęp teoretyczny Deklaracja funkcji:

zwracany_typ n a z w a _ f u n k c j i ( l i s t a argument ów)

Przekazywanie parametrów do funkcji

W C parametry do funkcji przekazywane są przez wartość. Oznacza to, że kopiowane są wartości argumentów aktualnych do argumentów formalnych utworzonych na stosie.

Przykład:

void f o o ( i n t f o o b a r ) {

f o o b a r += 4 ;

p r i n t f ( "%d\n " , f o o b a r ) ; }

Jeśli podaną funkcję wywołamy następująco:

(24)

i n t main ( ) {

i n t bar = 1 ; f o o ( ba r ) ;

p r i n t f ( "%d\n " , bar ) ; return 0 ;

}

to zostaną wyświetlone liczby 5 i 1, zatem zmienna bar z funkcji main() nie zostanie zmodyfikowana we funkcji foo(int foobar).

W C++ możliwe jest przekazywanie parametrów przez referencję (przezwisko). Przy- kład referencji:

void f o o ( ) {

i n t b a r 1 = 1 ; i n t &b a r 2 = b a r 1 ; b a r 1 = 3 ;

p r i n t f ( "%d\n " , b a r 2 ) ; }

W powyższym przykładzie zostanie wyświetlona liczba 3.

Przekazywanie parametrów przez referencję oznacza to, że kopiowany jest adres zmiennej na stos. W podanym przykładzie funkcja main() pozostaje nie zmieniona, zaś funkcja foo wygląda następująco:

void f o o ( i n t &f o o b a r ) {

f o o b a r += 4 ;

p r i n t f ( "%d\n " , f o o b a r ) ; }

Zostaną wyświetlone liczby 5 i 5. Konsekwencją jest zatem zmiana wartości zmiennej bar we funkcji foo(int &foobar). Czytamy zmienna foobar jest referencją (przezwiskiem) do zmiennej typu int.

Referencja ma tę cechę, że jest stałym przezwiskiem, a więc nie można spowodować, że dana referencja stanie się przezwiskiem innej zmiennej. Przykład:

void f o o ( ) {

i n t b a r 1 = 1 ; i n t b a r 2 = 2 ; i n t &b a r 3 = b a r 1 ; i n t &b a r 4 = b a r 2 ; b a r 3 = b a r 4 ;

p r i n t f ( "%d␣%d␣%d␣%d\n " , bar1 , bar2 , bar3 , b a r 4 ) ;

}

(25)

Powyższa funkcja wyświetli cztery razy 2. Zmienna bar3 jest referencją do zmiennej bar1. Zmienna bar4 jest referencją do zmiennej bar2. Instrukcja przypisania nie może spowodować, że bar3 stanie się referencją do tego samego co bar4, ze względu na wspo- mnianą wyżej cechę referencji. Powyższa funkcja poprawnie się skompiluje, a operacja przypisania będzie polegała na skopiowaniu wartości zmiennej bar2 do zmiennej bar1.

Ten sam efekt uzyskalibyśmy również poprzez przypisanie bar3 = bar2, w tym wypadku do zmiennej wskazywanej przez bar3 kopiujemy wartość zmiennej bar2.

Stała referencja. Stała referencja oznacza, że nie życzymy sobie zmiany wartości zmiennej, na którą wskazuje referencja. Przykład:

void f o o ( ) {

i n t b a r 1 = 1 ; i n t b a r 2 = 2 ;

const i n t &b a r 3 = b a r 1 ; i n t &b a r 4 = b a r 2 ;

// b a r 3 = b a r 4 ; // b a r 3 = b a r 2 ; }

Obydwie zakomentowane instrukcje nie powiodą się, ponieważ nie możemy zmieniać wartości zmiennej wskazywanej przez bar3.

Możemy również tworzyć referencje do stałej, ale pod jednym warunkiem, ze względu na to, że nie możemy zmieniać stałej, referencja również powinna być stała:

const i n t &b a r 2 = 3 ;

Porównywanie referencji. Porównywane są zmienne, na które wskazują referen- cje. Przykład:

\ void f o o 5 ( ) {

i n t b a r 1 = 1 ; i n t b a r 2 = 1 ; i n t &b a r 3 = b a r 1 ; i n t &b a r 4 = b a r 2 ; i f ( b a r 3 == b a r 4 ) {

p r i n t f ( " Takie ␣ same \n " ) ; }

i f ( b a r 3 == b a r 2 ) {

p r i n t f ( " Takie ␣ same \n " ) ; }

}

(26)

W C++ możliwe jest stosowanie argumentów domniemanych, co oznacza możliwość pominięcia podawania argumentów aktualnych w wywołaniu funkcji. Przykład:

void f o o ( i n t f o o b a r = 0 ) {

f o o b a r += 4 ;

p r i n t f ( "%d\n " , f o o b a r ) ; }

Możliwe jest wywołanie powyższej funkcji następująco: foo(). Wtedy zostanie przyjęte, że foobar ma wartość 0.

Techniki programowania

• Zmienne małych rozmiarów mogą być w praktyce przekazywane przez wartość, zmienne większych rozmiarów powinny być przekazywane przez referencję.

• sprawdzanie poprawności argumentów funkcji

• stosowanie const wszędzie gdzie spodziewamy się, że zmienna nie będzie modyfi- kowana

1.5.2 Zadania Zadania na 3.0

Napisz funkcję, która modyfikuje dwa parametry aktualne tej funkcji, w ten sposób, że zamienia wartości miejscami. Napisać dwie funkcje, jedna dla argumentów typu int, a druga dla argumentów typu double.

Zadania na 4.0

Czy poprawna jest następująca funkcja:

i n t& f o o ( ) {

i n t bar = 9 ; return bar ; }

}

Co robi powyższa funkcja? Czy używanie powyższych konstrukcji jest błędem? Jeśli tak, to dlaczego?

Zadania na 5.0

Czy poprawna są funkcje:

(27)

void f o o ( ) {

}

void bar ( ) {

return f o o ( ) ; }

Uzasadnić odpowiedź.

Zadania na 6.0

Co oznacza podany zapis? Do czego mógłby się przydać?

typedef i n t (& r i f i i ) ( int , i n t ) ;

1.6 Tablice

1.6.1 Wstęp teoretyczny Tablice jednowymiarowe

Elementy tablicy zajmują obszar ciągły w pamięci.

Definicja tablicy:

i n t n a z w a T a b l i c y [ 3 ] ;

Definicja tablicy wraz z inicjalizacją:

i n t t a b 1 [ ] = { 1 , 2 , 3 , 4 , 1 , 1 , 1 } ;

Tablica może być również zainicjalizowana za pomocą pętli:

i n t r o z m i a r = 4 ; i n t t a b 3 [ r o z m i a r ] ;

f o r ( i n t i = 0 ; i < r o z m i a r ; i ++) {

t a b 3 [ i ] = 0 ; }

Począwszy od standardu C99 obsługiwane są tablice o rozmiarze ustalanym w trakcie wykonywania programu, a nie podczas kompilacji.

W C zazwyczaj nie jest możliwe przypisanie tablicy do tablicy. Porównywanie tablic jest porównaniem adresu początku tablicy.

Przekazywanie tablicy w parametrach funkcji. Przekazanie tablicy o dowolnym roz-

miarze:

(28)

void f u n k c j a 1 ( i n t t a b 1 [ ] , i n t rozmiarTab1 ) {

t a b 1 [ 0 ] = 1 0 ; }

Można również wpisać konkretny rozmiar tablicy do argumentu formalnego. W przy- padku tablic nie ma potrzeby przekazywania tablicy przez referencję.

Stała tablica oznacza, że nie mogą być zmieniane wartości elementów.

Zwracanie tablicy. W C nie jest możliwe zwrócenie tablicy w rezultacie funkcji.

Sposobem na zwrócenie tablicy jest podanie tablicy wynikowej w argumentach funkcji.

void f u n k c j a 1 ( i n t t a b 1 [ ] , i n t s i z e T a b 1 , i n t t a b R e s u l t [ ] , i n t s i z e T a b R e s u l t ) {

t a b R e s u l t [ 0 ] = t a b 1 [ 0 ] ; }

Tablice wielowymiarowe

Inicjalizacja tablicy dwuwymiarowej:

i n t tab [ ] [ 3 ] = { { 1 , 1 , 3 } , { 2 , 3 , 3 } } ; Inicjalizacja tablicy trójwymiarowej:

i n t tab [ ] [ 1 ] [ 1 ] = { { { 1 } } , { { 2 } } } ;

Przy inicjalizacji nie jest konieczne podawanie ilości wierszy tablicy, ponieważ wystarczy podać te rozmiary aby można było później dostać się do elementu tablicy [i][j]. Dlatego musimy podać liczbę kolumn, a nie musimy liczby wierszy.

Inicjalizacja tablicy dwuwymiarowej za pomocą pętli:

i n t i l o s c W i e r s z y = 2 ; i n t i l o s c K o l u m n = 3 ;

i n t tab [ i l o s c W i e r s z y ] [ i l o s c K o l u m n ] = { { 1 , 1 , 3 } , { 2 , 3 , 3 } } ; f o r ( i n t i = 0 ; i < i l o s c W i e r s z y ; i ++)

{

f o r ( i n t j = 0 ; j < i l o s c K o l u m n ; j ++) {

t a b [ i ] [ j ] = 0 ; }

}

Stałe napisowe. „Napis” jest tablicą znaków zakończoną znakiem ’\0’. Do biblioteki

standardowej należą funkcje, które operują na tablicach znaków, np. funkcja strlen

zwracająca długość napisu. Porównywanie napisów zależne od implementacji.

(29)

Sito Eratostenesa

Za pomocą sita Eratostenesa znajdujemy liczby pierwsze z przedziału od 2 do określonej liczby.

i n t t a b S i z e = 1 0 0 ; _Bool t a b [ t a b S i z e ] ; t a b [ 0 ] = f a l s e ; t a b [ 1 ] = f a l s e ;

f o r ( i n t i = 2 ; i < t a b S i z e ; i ++) {

t a b [ i ] = true ; }

i n t u p p e r L i m i t = s q r t ( t a b S i z e ) ;

f o r ( i n t i = 2 ; i <= u p p e r L i m i t ; i ++) {

i f ( tab [ i ] == true ) {

f o r ( i n t j = i + i ; j <= t a b S i z e ; j += i ) {

t a b [ j ] = f a l s e ; }

} }

f o r ( i n t i = 0 ; i <= t a b S i z e ; i ++) {

i f ( tab [ i ] == true ) {

p r i n t f ( "%d␣ " , i ) ; }

}

1.6.2 Zadania Zadania na 3.0

a) Odczytac ze standardowego wejscia kilka napisów i umieścić je w tablicy dwuwymia- rowej. Utworzyć drugą tablicę dwuwymiarową, w której należy umieścić palindromy do podanych na wejściu napisów. Wyświetlić tablicę wynikową.

Zadania na 4.0

Napisać funkcję, która przyjmuje tablicę wypełnioną 0 lub 1, i dodaje binarnie liczbę 1

i zwraca tablicę z cyframi wyniku dodawania. Napisać drugą funkcję, która dla każdej

(30)

liczby podanej w tablicy w postaci binarnej wypisuje ją na ekran. Parametrem tej funkcji jest rozmiar tablicy.

Zadania na 5.0

Tablica dwuwymiarowa może być zaimplementowana za pomocą tablicy jednowymia- rowej. Zaimplementować funkcję get przyjmującą tablicę jednowymiarową, w której znajdują się elementy tablicy dwuwymiarowej, która ponadto przyjmuje współrzędne x, y tablicy dwuwymiarowej i zwraca wartość komórki x, y tej tablicy. Zadanie dodatkowe:

to samo dla tablicy trójwymiarowej.

1.7 Wyszukiwanie i sortowanie

1.7.1 Wstęp teoretyczny

1.7.2 Algorytmy wyszukiwania

Sprawdzenie czy element znajduje się w tablicy:

_Bool sprawdzElement ( i n t t a b [ ] , i n t s i z e , i n t e l e m e n t ) {

f o r ( i n t i = 0 ; i < s i z e ; i ++) {

i f ( tab [ i ] == e l e m e n t ) {

return true ; }

}

return f a l s e ; }

Znajdowanie najmniejszego i największego elementu w zbiorze.

void f i n d E x t r e m a ( i n t tab [ ] , i n t s i z e ) {

i n t max = tab [ 0 ] ; i n t maxIndex = 0 ; i n t min = tab [ 0 ] ; i n t minIndex = 0 ;

f o r ( i n t i = 1 ; i < s i z e ; i ++) {

i f ( tab [ i ] > max ) {

max = t a b [ i ] ;

maxIndex = i ;

}

(31)

i f ( tab [ i ] < min ) {

min = t a b [ i ] ; minIndex = i ; }

}

p r i n t f ( " min : ␣%d␣ minIndex : ␣%d\n " , min , minIndex ) ; p r i n t f ( " max : ␣%d␣ maxIndex : ␣%d\n " , max , maxIndex ) ; }

Powyższy algorytm w przypadku pesymistycznym wykonuje maksymalnie 2 (n + 1) (n = size) operacji przypisania oraz 3 (n − 1) operacji porównania.

1.7.3 Algorytmy sortowania Sortowanie bąbelkowe

void babelkowe ( i n t tab [ ] , i n t s i z e ) {

f o r ( i n t j = 0 ; j < s i z e − 1 ; j ++) {

f o r ( i n t i = 0 ; i < s i z e − 1 ; i ++) {

i f ( tab [ i ] > tab [ i + 1 ] ) {

i n t temp = tab [ i ] ; t a b [ i ] = t a b [ i + 1 ] ; t a b [ i +1] = temp ; }

} } }

Sortowanie bąbelkowe polega na sortowaniu par elementów leżących obok siebie.

Analizujemy kolejno po kolei wszystkie pary. Czynność tą powtarzamy. Można zauwa- żyć, że po każdym pojedynczym przejściu będziemy mieć o jeden więcej element posor- towany patrząc od końca tablicy. A więc w kolejnych przejściach nie trzeba sortować końcowych elementów:

void babelkowe ( i n t tab [ ] , i n t s i z e ) {

f o r ( i n t j = s i z e − 1 ; j > 0 ; j −−) {

f o r ( i n t i = 0 ; i < j ; i ++) {

i f ( tab [ i ] > tab [ i + 1 ] )

(32)

{

i n t temp = tab [ i ] ; t a b [ i ] = t a b [ i + 1 ] ; t a b [ i +1] = temp ; }

} } }

Liczba porównań wynosi:

n 2 − n

2 − 1

Dla pesymistycznego przypadku liczba zamian wynosi tyle samo. Przypadek pesymi- styczny zachodzi, gdy elementy tablicy są posortowane wcześniej w odwrotnej kolejności.

Sortowanie przez wstawianie

void w s t a w i a n i e ( i n t tab [ ] , i n t s i z e ) {

i n t e l e m e n t ;

f o r ( i n t i = 1 ; i < s i z e ; i ++) {

e l e m e n t = t a b [ i ] ; i n t j ;

f o r ( j = i − 1 ; j >= 0 && tab [ j ] > e l e m e n t ; j −−) {

t a b [ j + 1 ] = t a b [ j ] ; }

t a b [ j + 1 ] = e l e m e n t ; }

}

Algorytm wstawiania polega na podziale tablicy na dwie części, gdzie pierwsza część jest posortowana. Na początku pierwsza część zawiera tylko pierwszy element. Następnie wstawiamy kolejny element z drugiej części do pierwszej w odpowiednie miejsce, prze- suwając odpowiednio elementy za wstawianym miejscem z pierwszej części, itd. Podany wyżej algorytm zawiera taką optymalizację, że wyszukiwanie miejsca gdzie należy wsta- wić element i przesuwanie elementów jest wykonywane w tej samej pętli. Maksymalna liczba operacji porównywania elementów tablicy:

n 2 − n 2 Maksymalna liczba operacji przypisań

n 2 − n

2 + n − 1

(33)

Sortowanie metodą wstawiania połówkowego

i n t z n a j d z M i e j s c e ( i n t l i m i t G o r n y , i n t tab [ ] , i n t s i z e , i n t e l e m e n t ) {

i n t lewy = 0 ;

i n t prawy = l i m i t G o r n y ; while ( lewy <= prawy ) {

i n t m = ( lewy + prawy ) / 2 . 0 ; i f ( e l e m e n t < tab [m] )

{

prawy = m − 1 ; } e l s e

{

lewy = m + 1 ; }

}

return lewy ; }

void wstawianiePolowkowe ( i n t tab [ ] , i n t s i z e ) {

i n t e l e m e n t ;

f o r ( i n t i = 1 ; i < s i z e ; i ++) {

e l e m e n t = t a b [ i ] ; i n t j ;

i n t m i e j s c e = z n a j d z M i e j s c e ( i − 1 , tab , s i z e , e l e m e n t ) ; f o r ( j = i − 1 ; j >= m i e j s c e ; j −−)

{

t a b [ j + 1 ] = t a b [ j ] ; }

t a b [ m i e j s c e ] = e l e m e n t ; }

}

Sortowania metodą wstawiania połówkowego jest modyfikacją sortowania przez wsta- wianie, polegającą na wstawianiu elementu za pomocą algorytmu połówkowego. Algo- rytm połówkowy polega na tym, że porównujemy element, który chcemy wstawić z ele- mentem leżącym w połowie przedziału. Przez to porównanie wiemy, w której połowie znajduje się miejsce wstawienia. Następnie wybraną połowę jeszcze raz dzielimy na pół, itd. Aż otrzymamy miejsce, gdzie należy wstawić element.

(34)

Sortowanie przez wybór

void wybor ( i n t tab [ ] , i n t s i z e ) {

f o r ( i n t i = 0 ; i < s i z e − 1 ; i ++) {

i n t minIndex = i ;

f o r ( i n t j = i + 1 ; j < s i z e ; j ++) {

i f ( tab [ j ] < tab [ i ] ) {

minIndex = j ; }

}

i f ( minIndex != i ) {

i n t temp = tab [ i ] ; t a b [ i ] = t a b [ minIndex ] ; t a b [ minIndex ] = temp ; }

} }

W sortowaniu przez wybór również tablica podzielona jest na dwie części, pierwsza część jest posortowana. Wyszukujemy minimum z drugiej części i zamieniamy to minimum z pierwszym elementem drugiej części i ten element staje się elementem pierwszej części.

Maksymalna liczba porównań elementów tablicy:

(n − 1) n 2 Maksymalna liczba zamian wynosi:

n − 1 1.7.4 Zadania

Zadania na 3.0

Zrób zestawienie liczby operacji w przypadku najbardziej optymistycznym dla metod sortowania przedstawionych na zajęciach. Wskaż wszystkie przypadki optymistyczne dla każdej metody.

Zadania na 4.0

Przeanalizuj dla jakiego rodzaju danych każda z metod jest najszybsza i dlaczego.

(35)

Zadania na 5.0

Dodaj do sortowania bąbelkowego dwa udoskonalenia: 1. zapamiętanie czy dokony- wano w poprzednim kroku jakiejkolwiej zmiany, jeśli nie dokonano to algorytm może się zakończyć, 2. zapamiętać pozycję ostatniej zmiany, i wykorzystać ją do pominięcia sprawdzania par, które występują wcześniej.

Zadania na 6.0

Zaimplementuj sortowanie mieszane, które jest modyfikacją sortowania bąbelkowego, która polega na zmiany kierunku kolejnych przejść.

1.8 Złożoność obliczeniowa

1.8.1 Wstęp teoretyczny

Złożoność obliczeniowa algorytmu to ilość zasobów komputerowych potrzebnych do jego wykonania. Podstawowymi rozważanymi zasobami są czas działania i ilość zajmo- wanej pamięci. Złożoność obliczeniowa to funkcja rozmiaru danych wejściowych, np dla algorytmów sortowania za rozmiar przyjmuje się liczbę elementów w ciągu wejściowym.

Na złożoność obliczeniową składa się złożoność pamięciowa i złożoność czasowa.

Jednostką złożoności pamięciowej będzie słowo pamięci maszyny. Jednostką złożoności czasowej będzie jedna operacja dominująca. Łączna liczba operacji dominujących jest proporcjonalna do liczby wykonań wszystkich operacji jednostkowych. Dla algorytmów sortowania przyjmuje się za operację dominującą zwykle porównanie dwóch elementów w ciągu wejściowym, a czasem też przestawienie elementów w ciągu. Dla algorytmów numerycznych operacjami dominującymi są dodawanie i mnożenie.

Wyróżniamy złożoność pesymistyczną i oczekiwaną. Złożoność pesymistyczna definiowana jest jako ilość zasobów komputerowych potrzebnych przy „najgorszych” da- nych wejściowych. Złożoność oczekiwana definiowana jest jako ilość zasobów kompu- terowych potrzebnych przy „typowych” danych wejściowych.

Dokładna definicja złożoności pesymistycznej:

D n - zbiór zestawów danych wejściowych rozmiaru n,

t (d) - liczba operacji dominujących dla zestawu danych wejściowych d, Pesymistyczna złożoność czasowa algorytmu to funkcja:

W (n) = sup {t (d) : d ∈ D n }

Oczekiwana złożoność czasowa w przypadku gdy wszystkie dane są jednakowo prawdo- podobne (z prawdpodobieństwem niezerowym) wyraża się wzorem:

A (n) = P

d∈D

n

t (d)

|D n |

Przykład. Dla algorytmu wyszukiwania elementu w tablicy z poprzednich zajęć ope-

racją dominującą jest porównanie, złożoność pesymistyczna wynosi n. Jeśli element

(36)

zostanie znaleziony na k-tym miejscu to liczba operacji dominujących wynosi k. Za- tem liczba operacji dominujących waha się w granicy od 1 do n. A zatem złożoność oczekiwana wynosi:

A (n) = 1 + 2 + . . . + n

n = n + 1

2 Notacja „O”

Faktyczna złożoność czasowa algorytmu w chwili jego użycia różni się od wyliczonej teo- retycznie współczynnikiem proporcjonalności, który zależy od konkretnej realizacji tego algorytmu. Istotną informacją zawartą w pesymistycznej i oczekiwanej złożoności czaso- wej jest rząd wielkości, czyli zachowanie asymptotyczne, gdy n dąży do nieskończoności.

Zwykle staramy się podać jak najprostszą funkcję charakteryzującą rząd wielkości W (n) i A (n), na przykład n, n log n, n 2 , n 3 . Niech:

f, g, h : N → R + ∪ {0}

Funkcja f jest co najwyżej rzędu g, co zapisujemy f (n) = O (g (n)), jeśli istnieją stała rzeczywista c > 0 i stała naturalna n 0 takie, że nierówność f (n) ≤ cg (n) zachodzi dla każdego n ≥ n 0 .

Przykład: n 2 + n = O n 2  , ponieważ n 2 + 2n ≤ 3n 2 .

Funkcja f jest co najmniej rzędu g, co zapisujemy f (n) = Ω (g (n)), jeśli g (n) = O (f (n)).

Funkcja f jest dokładnie rzędu g, co zapisujemy f (n) = Θ (n), jeśli zarówno f (n) = O (g (n)), jak i f (n) = Ω (g (n)). Poprawny jest też termin f jest asymptotycznie rów- noważne g i oznaczenie f (n) ∼ = g (n). Przykład: n 2 + 2n ∼ = n 2 , ponieważ n 2 + 2n ≤ 3n 2 jak i n 2 + 2n ≥ n 2 dla każdego n > 0.

Uproszczenie funkcji opisującej rząd wielkości polega na pominięciu wszystkich skład- ników funkcji oprócz tego, który ma największy wpływ, pominięciu wszelkich stałych współczynników.

Popularne rzędy wielkości:

• złożoność stała O (1) - stała liczba operacji dominujących bez względu na rozmiar danych wejściowych Przykład sprawdzenia czy liczba jest parzysta.

• złożoność logarytmiczna O (log n) - Np algorytmy w których problem postawiony dla danych rozmiaru n da sie sprowadzić do problemu z rozmiarem danych o połowę mniejszym. Przykład: znalezienie elementu w posortowanej tablicy za pomocą binarnego wyszukiwania.

• złożoność liniowa, O (n) - dla każdej danej wykonywana jest stała liczba operacji, np znalezienie elementu w nieposortowanej liście

• złożoność liniowo-logarytmiczna, O(n log n), Np. gdy problem o rozmiarze n da się sprowadzić do problemu o rozmiarze n/2 w n krokach

• złożoność kwadratowa: np algorytm sortowanie przez wstawianie

(37)

• złożoność wykładnicza 2 n : sprawdzenie czy dla danej formuły logicznej istnieje takie podstawienie zmiennych zdaniowych, żeby formuła była prawdziwa

• złożoność wykładnicza n!: np. znalezienie optymalnego rozwiązania problemu komiwojażera(znaleźć najkrótszą drogę z miasta A do B, przechodząc przez każde miasto tylko raz)

1.8.2 Zadania Zadania na 3.0

Podaj pesymistyczną złożoność algorytmów sortowania rozpatrywanych na poprzednich zajęciach w notacji duże O.

Zadania na 4.0

Określić liczbę operacji dominujących w zwykłym wyszukiwaniu i wyszukiwaniu z war- townikiem, polegającym na dodatniu na końcu tablicy elementu poszukiwanego i pomi- nięciu sprawdzania, czy doszliśmy do końca tablicy. Podać złożoność obliczeniową tych dwóch algorytmów w notacji duże O. O ile skraca się czas wyszukiwania?

Zadania na 5.0

Udowodnij, że dolnym ograniczeniem przeszukiwania binarnego jest log 2 n.

1.9 Złożoność obliczeniowa, cd

1.9.1 Wstęp teoretyczny Theorem 1.9.1. Gdy

f (n) = an 2 + bn + c gdzie a,b,c są stałymi, przy czym a > 0 zachodzi:

f (n) = Θ  n 2  Theorem 1.9.2. Gdy

p (n) =

d

X

i=0

a i n i gdzie a i są stałymi i a d > 0, mamy:

p (n) = Θ  n d 

(38)

Theorem 1.9.3. Gdy

f (n) = an + b to

f (n) = O  g  n 2 

Dowód.

c = a + |b|

an + b <= (a + |b|) n 2 dla n 0 = 1

Uwaga: Dla dwóch funkcji f (n) i g (n) może nie wystąpić żaden z dwóch przypadków:

ani f (n) = O (g (n)), ani f (n) = Ω (g (n)). Na przykład funkcji n oraz n 1+sin n nie można w tym sensie porównać. Wartość wykładnika waha się od 0 do 2.

Analiza czasu działania algorytmu przez wstawianie

Pesymistyczny czas działania wynosi O n 2  . Optymistyczny czas działania wynosi Ω (n).

Oszacowania te są najlepsze z możliwych.

Notacja o

o (g (n)) = {∀ c>0n

0

>0n≥n

0

0 ≤ f (n) < cg (n)}

Ograniczenie to nazywamy asymptotycznie dokładnym.

2n = o  n 2  2n 2 6= o  n 2  Notacja ω

ω (g (n)) = {∀ c>0n

0

>0n≥n

0

0 ≤ cg (n) < f (n)}

Przykładowo:

n 2

2 = ω (n) n 2

2 6= ω  n 2  Własności notacji asymptotycznych

Przechodniość, zwrotność, symetria w przypadku notacji Θ, symetria transpozycyjna.

(39)

1.9.2 Zadania Zadania na 3.0

Wyjaśnij dlaczego stwierdzenie „Czas działania algorytmu A wynosi co najmniej O n 2  nie ma sensu.

Zadania na 4.0

Pokaż, że dla dowolnych rzeczywistych stałych a i b, gdzie b > 0, zachodzi (n + a) b = Θ  n b 

Zadania na 5.0 Sprawdź czy

2 n+1 = O (2 n ) oraz

2 2n = O (2 n ) Zadania na 6.0

Uporządkuj następujące funkcje ze względu na ich rząd wielkości.

log 2 (log 2 ∗n) , 2 log

2

∗n , 

2  log

2

n , n 2 , n!, (log 2 n)!

 3 2

 n

, n 3 , log 2 2 n, log 2 (n!) , 2 2

n

, n

1 log2 n

ln ln n, log 2 ∗n, n2 n , n log

2

log

2

n , ln n, 1 2 log

2

n , (log 2 n) log

2

n , e n , 4 log

2

n , (n + 1)! 1

p log 2 n log 2 ∗ (log 2 n), 2

2 log

2

n , n, 2 n , n log 2 n, 2 2

n+1

Wyrażenie log 2 ∗n oznacza tzw. logrytm iterowany i jest zdefiniowane następująco:

log 2 ∗n = min n i ≥ 0 : log 2 (i) n ≤ 1 o

przy czym zapis f (i) n oznacza funkcję f (n) zastosowaną iteracyjnie i razy do wartości

początkowej n.

Cytaty

Powiązane dokumenty

Ścieżka Hamiltona jest ścieżką, która przechodzi dokładnie jeden raz przez każdy wierzchołek grafu.. Przykłady i algorytmy

rozkazów zapisanych w postaci mnemoników, np.: mov ax, bx add ax, 1 Języki trzeciej generacji – to języki proceduralne, imperatywne i obiektowe pozwalające na

 Szyfr Playfaira (stworzony przez Charlsa Wheatstone’a) opierał się na tablicy 5x5, w którą wpisywano słowo klucz, a następnie kolejne litery alfabetu w porządku

Lista, krotka i słownik – to sekwencje, których elementy są ponumerowane, więc można się do nich odwoływać po indeksie lub po nich iterować pętlą for … in …:. STRUKTURY

W celu ujednolicenia obliczeń numerycznych na różnych platformach sprzętowych wprowadzono standard IEEE 754 dla zapisu liczb.. zmiennoprzecinkowych (o

• prawdziwa - gdy zbiór danych składających się na tą informację jest skojarzony ze sobą w sposób zgodny względem innych informacji składających się na wiarygodną

Wyszukiwanie z wykorzystaniem tablicy haszującej (hash table) umożliwia teoretycznie najszybsze wyszukiwanie (w czasie stałym) pod warunkiem określenia takiej funkcji haszującej,

Wartością wskaźnika jest natomiast adres pamięci RAM, gdzie znajduje się taka zmienna...