Download as pdf or txt
Download as pdf or txt
You are on page 1of 46

WPROWADZENIE DO ALGORYTMIKI I PROGRAMOWANIA

1
Andrzej Handkiewicz

Akademia im. Jakuba z Paradyża, Gorzów Wlkp.

1 e-mail: AHandkiewicz@cs.put.poznan.pl

1
1 Wstęp
Komputer jest najważniejszym narzędziem w pracy umysłowej człowieka. Dzięki rozwojowi technologii mikro-
elektronicznych stał się on urządzeniem tanim, podręcznym i ogólnie dostępnym. Dla większości użytkowników
komputer służy do gier, oglądania filmów, korzystania z różnego rodzaju stron dostępnych w sieci, włączania się
do środowisk społecznościowych i podobnych pasywnych form jego wykorzystania. Aktywne podejście do pracy
na komputerze polega głównie na umiejętności jego programowania, co wymaga jednak opanowania określonego
zakresu wiedzy. Najważniejsze etapy związane z tzw. procesem programowania komputerów zostały przedsta-
wione na rys.1. Centralną rolę w procesie komunikowania się z komputerem odgrywa język programowania. W
ramach tego przedmiotu będziemy się opierać na języku C ++ , którego znajomość jest przyjęta w informatyce
jako umiejętność podstawowa.

Rysunek 1: Główne etapy programowania

Można doliczyć się nawet kilkuset języków programowania. Porównanie jedynie trzech z nich: P ascala, C
oraz C ++ , w prostym przykładzie obliczania pola i obwodu koła, pokazuje ideę programowania w językach
wysokiego poziomu. Wyjaśnienie nazwy P ascal wkrótce się pojawi. Dwa plusy ++ w nazwie C ++ oznacza
ulepszoną wersję języka C, który jest obecnie wykorzystywany głównie na poziomie sprzętowym.

P ascal repeat
write('Podaj dlugosc promienia, r = ');
readln(r);
if r < 0 then
writeln('Blad. Promien musi byc dodatni!');
until r>= 0;
if r > 0 then
begin
writeln('Pole kola =', 3.14159 * r * r:0:4);
writeln('Obwod kola =', 6.28319 * r:0:4);
end;

C, C ++ do {
printf("Podaj dlugosc promienia, r = ");
scanf("%lf", &r);
if r < 0
pritf("Blad. Promien musi byc dodatni!\n");
} while r < 0;
if r > 0
{
printf("Pole kola = %0.4lf\n", 3.14159 * r * r);
printf("Obwod kola = %0.4lf\n", 6.28319 * r);
}

C ++ do {

2
cout << "Podaj dlugosc promienia, r = ";
cin >> r;
if r < 0
cout >> "Blad. Promien musi byc dodatni!\n";
} while r < 0;
if r > 0
{
cout << "Pole kola = ", << 3.14159 * r * r << endl;
cout << "Obwod kola = ", << 6.28319 * r << endl;
}

Język C ++ ma składnię o złożoności podobnej jak P ascal lub C ale bardziej rozbudowanych strukturach
danych. Wykorzystanie takich złożonych struktur daje jednak znaczne ułatwienia w programowaniu, widoczne
w powyższych przykładach w operacjach wejścia/wyjścia. Są one oznaczone operatorami przesunięcia « oraz
», odpowiednio w lewo lub prawo, a przez bibliotekę wejścia/wyjścia iostream zostały w naturalny sposób
wypożyczone. W ramach przedmiotu obejmującego wstęp do programowania omówimy jedynie jego najbar-
dziej podstawowe elementy, pozostawiając możliwość pogłębienia C ++ w ramach innych przedmiotów, a przede
wszystkim podczas samokształcenia. Posługiwanie się językiem ze zrozumieniem jest związane ze znajomością
pozostałych etapów programowania, które zostaną pokrótce omówione w kolejnych punktach.

2 Algorytm
Algorytm to procedura (przepis) na wykonanie zadania w jakiejś dziedzinie. Jego opracowaniem powinien się
więc zajmować nie tyle programista ile ekspert (specjalista) w danej dziedzinie. Proste algorytmy można przed-
stawiać na schemacie blokowym. Jednak w zagadnieniach o dużej złożoności ten sposób jest mało przejrzysty i
korzysta się z opisu tekstowego. Dużym ułatwieniem zarówno dla eksperta jak i programisty jest opis tekstowy
algorytmu bliski językowi programowania, który zostanie wykorzystany. Wskazane jest więc aby ekspert znał
się na podstawach programowania, albo odwrotnie, programista był również specjalistą w dziedzinie, w której
programuje. Należy zdawać sobie sprawę, że wiele problemów może być rozwiązanych jedynie przez zespoły
ekspertów i programistów, którzy muszą posiąść umiejętność efektywnej współpracy.
Złożoność obliczeniowa jest definiowana parametrem, za pomocą którego można określić czas wykonania
algorytmu jako funkcji, której argument odpowiada rozmiarowi danych wejściowych. Parametr ten nie dotyczy
jednak zasobów związanych z komputerem, nie zależy od języka programowania czy kompilatora, lecz jest cechą
samego algorytmu. Pozwala porównywać różne problemy i skonstruowane do ich rozwiązania algorytmy pod
kątem trudności w ich rozwiązywaniu.
Dla opisu złożoności obliczeniowej algorytmu stosowane są różne notacje, które jedynie wymienimy, bez
szczegółowego ich opisu. Np. notacja duże-O opisuje najgorszy przypadek, jaki może wystąpić w związku z
użytym algorytmem. Oznaczamy tę notację jako O(g(n)), gdzie n jest rozmiarem danych wejściowych. O(g(n))
oznacza zbiór funkcji f (n) spełniających warunek, że czas przebiegu algorytmu jest dla n ≥ n0 nie większy niż
funkcja g(n) pomnożona przez określony współczynnik c, czyli
O(g(n)) = {∃c > 0 ∀n ≥ n0 : 0 ≤ f (n) ≤ cg(n)} (1)
Dualnym parametrem dla O(g(n)) określającym górną granicę czasu przebiegu algorytmu jest Ω(g(n)) okre-
ślającym dolną granicę tego czasu. Z kolei parametr Θ(g(n)) definiuje obie granice: dolną i górną. Tych i innych
podobnych parametrów nie będziemy definiować. Mają one znaczenie dla tych ekspertów i programistów, którzy
opracowują algorytmy rozwiązujące określone problemy.
Konstruowanie efektywnych algorytmów wymaga więc nie tylko wiedzy eksperckiej, ale również dobrych
podstaw teoretycznych. Podstawową rolę odgrywają w tym przypadku modele matematyczne problemów, które
mają być zalgorytmizowane. Konieczna jest znajomość wielu dziedzin matematyki, takich jak logika, teoria
grafów, algebra macierzy, równania różniczkowe, itp. Jednak każdy specjalista oprócz tej wiedzy powinien mieć
świadomość, że źle sformułowany problem może być nierozwiązywalny nawet przy użyciu najmocniejszych su-
perkomputerów. Bardzo pomocna jest w tym przypadku znajomość podstawowych pojęć teorii algorytmów,
w szczególności złożoności obliczeniowej. Do problemu złożoności obliczeniowej algorytmów powrócimy więc w
sekcji 9 na końcu skryptu, po zapoznaniu się z językiem programowania C ++ .

3
3 Procesor
Jak to zostało już wcześniej powiedziane, komputer jest po zaprogramowaniu narzędziem wykonującym po-
stawione zadanie. Jednak wszyscy użytkownicy muszą mieć świadomość, że nawet w przypadku największych
superkomputerów ich możliwości są też ograniczone. Podstawową rolę odgrywają w tym przypadku procesory
użyte w komputerze. Architekturze komputerów jest poświęcony osobny przedmiot. W tym rozdziale omówimy
jedynie pokrótce budowę i działanie procesora. Warto w tym miejscu wspomnieć, że jako pierwszego projektanta
komputera (machina arytmetyczna) uznaje się filozofa i matematyka Blaise’a Pascala (1623-1662). Urządzenie
zostało skonstruowane przez Pascala, po samodzielnych studiach matematycznych i praktyce zegarmistrzow-
skiej, w wieku zaledwie 18-tu lat. Pascal zbudował około 50-ciu takich maszyn, które jednak, ze względu na
mechaniczną zawodność, się nie przyjęły. Dla upamiętnienia jego zasług, opracowany przez Niklausa Wirtha
w 1970 roku język programowania został nazwany nazwiskiem filozofa. Object Pascal jest obecnie językiem
osadzonym w środowisku Delphi.

4
Rysunek 2: Transmisja szeregowa

Prosty układ transmisji szeregowej na rys.2 pokazuje współdziałanie podstawowych elementów będących
również składnikami procesora: przerzutników, rejestrów i jednostki kontrolnej CU (automat skończony, maszyna
stanów). Wymiana zawartości rejestrów między układem nadrzędnym (master device) a podrzędnym (slave
device) wymaga jedynie n cykli zegara, gdzie n jest liczbą bitów rejestrów, przy jednobitowych szynach połączeń.

5
Rysunek 3: Budowa procesora

Rys.3 pokazuje, że procesor oprócz rejestrów i podstawowych bramek logicznych (AND, OR) zawiera jed-
nostkę arytmetyczno-logiczną (ALU). ALU jest zbudowane nie tylko z bramek logicznych, ale także sumatora
a nawet układu mnożącego. Oznacza to, że procesor może realizować sprzętowo, czyli z użyciem bramek logicz-
nych, nie tylko operacje logiczne, ale również arytmetyczne: dodawanie a nawet mnożenie. Wykonywana przez
ALU operacja jest określona przez kombinację zero-jedynkową na wejściach C0 , C1 . Sygnały te, jak i pozostałe
XA, XB, LA, LB, LOU T , XIN sterujące procesorem, są wytwarzane w kolejnych cyklach zegara przez CU w
zależności od instrukcji kodu (Instruction Code) na wejściu.

6
Rysunek 4: Działanie jednostki sterującej

Rys.4 przedstawia CU, który jak już wspomnieliśmy jest automatem skończonym, w postaci diagramu 7-miu
stanów. Jest to oczywiście bardzo uproszczona realizacja CU, zakładająca że kod instrukcji jest jednobitowy.
Zwykle kod instrukcji jest 8-mio bitowy i gdyby wszystkie jego kombinacje miały być wykorzystane, to automat
musiałby mieć przynajmniej 256 stanów.
Śledząc przykładowy diagram stanów łatwo zauważyć, że jeśli kod instrukcji jest zerowy, IN ST R = 0, to
działanie procesora polega na przeniesieniu danej z wejścia In do rejestru A, a następnie do wyścia Out.
Ćwiczenie
Jakie jest działanie procesora dla IN ST R = 1, zakładając, że C0 C1 = 00 oznacza w ALU sumowanie
arytmetyczne?

7
Rysunek 5: Procesor z pamięcią

Dane przetwarzane przez procesor są rzadko pobierane bezpośrednio z wejścia i wysyłane do wyjścia kom-
putera. Do ich przechowywania służy pamięć (Memory), tak jak to jest pokazane na rys.5. Taka architektura
komputera nosi nazwę architektury Princeton. Jeśli pamięć dla danych i instrukcji jest rozdzielona, tak jak to
przedstawiono na rys.6, to architektura jest nazywana Harward.

8
Rysunek 6: Architektura harwardzka

Każda pamięć ma budowę macierzową, której wiersze, w najbardziej podstawowym przypadku 1-bajtowe, są
nazywane słowami. W trakcie wykonywania algorytmu słowa, oznaczające odpowiednio dane lub kod instrukcji,
są kolejno odczytywane z pamięci. Można udowodnić, że ten sposób pobierania danych pozwala na wykonanie
dowolnego algorytmu.

9
Rysunek 7: Mikroprocesor

Mikroprocesorem nazywamy procesor, który wraz z CU, a często z pamięcią podręczną, jest wykonany na
wspólnym chipie w technologii CMOS. Komputer zawierający jeden lub więcej mikroprocesorów nazywamy
mikrokomputerem. Dopiero osiągnięcie technologii CMOS, ok. 300 lat po maszynie Pascala, pozwoliło na zbu-
dowanie taniego komputera o niemal 100% niezawodności.

10
4 Asembler

Jak już wspomnieliśmy opisując budowę procesora, jego instrukcje są jednobajtowymi liczbami w systemie
dwójkowym. Mnemoniki takich instrukcji, dla przykładowego procesora TI, są przedstawione w tabelach na ry-
sunkach 8 oraz 9. Odpowiadającą im liczbę dwójkową możemy znaleźć na podstawie liczb zapisanych w systemie
szesnastkowym (od 0 do F) w wierszach trzecim od góry lub ostatnim (bity bardziej znaczące) i w kolumnach
zewnętrznych (bity mniej znaczące). Potrzebne jest więc dodatkowe narzędzie, które program zapisany w języku
wysokiego poziomu przetłumaczy na jego zapis dwójkowy. Narzędzie to nazywamy asemblerem.

11
Rysunek 8: Mapa kodowa 1

Przykład 1
LDA oznacza ładowanie (load) akumulatora ACCA lub ACCB (rejestru A lub B), wskazanego w pierwszym
wierszu macierzy, daną ze słowa pamięci wskazanego w drugim wierszu.
Instrukcja zapisana mnemonikiem LDA jest liczbą, która w kodzie szesnastkowym odczytanym z odpowied-
niej kolumny i wiersza przyjmuje jedną z wartości 86, 96, A6, B6, dla akumulatora A lub C6, D6, E6, F6,
dla akumulatora B. W kodzie dwójkowym LDA jest więc zapisywane w 8-mio bitowym słowie pamięci jako
10000110, 10010110, 10100110, 10110110 lub 11000110, 11010110, 11100110, 11110110.
Przykład 2
ADD oznacza operację sumowania (addition).
Ćwiczenie
Jaki będzie zapis dwójkowy instrukcji ADD, jeśli tryb adresowania argumentów jest bezpośredni (DIR), a
wynik ma być umieszczony w akumulatorze B?

12
Rysunek 9: Mapa kodowa 2

Kolejne mnemoniki przykładowego procesora wymagające dopisania do instrukcji prefiksu.

13
Rysunek 10: Adresowanie pamięci

Na rys.10 wyjaśnione są tryby adresowania pamięci zapisane w drugim wierszu map kodowych przykładowego
procesora.
Adresowanie natychmiastowe (IMMEDIATE) oznacza, że w najbliższym po słowie pamięci z instrukcją
znajduje się słowo z jej argumentem. Jeśli argument wymaga większej liczby bitów, dana może być przechowy-
wana w dwóch lub więcej słowach pamięci. We wszystkich pozostałych trybach adresowania kolejne dwa słowa
(szyna adresowa jest 16-to bitowa) zawierają 8-mio bitowe liczby składające się na ten adres. Wyjątkiem jest
adresowanie bezpośrednie (DIRECT), w którym 8 najbardziej znaczących bitów adresu jest zerowych.
Ćwiczenie 1
Oblicz jaka może być maksymalna pamięć o jednobajtowych słowach adresowana szyną 16-to bitową.
Ćwiczenie 2
Jaka jest maksymalna liczba słów pamięci możliwa do zaadresowania szyną 4-ro bitową? Zaprojektuj de-
koder takiej pamięci, czyli układ kombinacyjny o wejściach z szyny adresowej i wyjściach odpowiadających
poszczególnym słowom.

14
5 Elementarz języka C ++
Programowanie procesora w asemblerze jest najbardziej efektywne pod względem szybkości działania i wy-
korzystania pamięci, ale niezwykle pracochłonne nawet przy wykorzystaniu mnemoników. Dlatego posługujemy
się tzw. językami wysokiego poziomu, do jakich należy C ++ .
W ramach tego przedmiotu zapoznamy się z podstawami tego języka posługując się środowiskiem Dev-C++
i kompilatorem gcc. Są to względnie proste narzędzia dostępne w różnych systemach operacyjnych, również w
wersji otwartej.

5.1 Funkcja
Podstawowym pojęciem języka C ++ jest funkcja, rozumiana tak jak w matematyce, jako relacja między jej
zmiennymi niezależnymi (argumentami) a zmienną zależną (wartość funkcji). Zmienne są wartościami przecho-
wywanymi w pamięci komputera. Nazwy funkcji i zmiennych są dowolnie nadawane przez programistę. Jedynym
ograniczeniem w wyborze nazw jest main() jako nazwa funkcji zastrzeżona dla całego programu oraz słowa
kluczowe (keywords). Jest możliwe napisanie programu, w którym użyto jedynie obligatoryjnej funkcji main()
i elementarnych pojęć, które obecnie omówimy. Program staje się jednak wtedy zawiły i mało zrozumiały. Do
strategii programowania bazującej na pojęciu funkcji powrócimy po wprowadzeniu najważniejszych elementów
języka.
Funkcje, których argumentami są obiekty, są również składowymi zaawansowanych typów, jakimi są klasy.
Pojęcie klasy, będące podstawą tzw. projektowania obiektowo orientowanego (object oriented program-
ming), zostanie krótko omówione na końcu skryptu.

5.2 Dyrektywy - funkcje w plikach bibliotecznych (nagłówkowych)


Środowisko, z którego korzysta programista, ma najczęściej wiele gotowych do użycia standardowych funk-
cji biblioteczych (stdlib) lub służących do komunikowania z terminalem (stdio). Są one umieszczone w tzw.
bibliotekach, nazywanych też plikami nagłówkowymi (header), które trzeba dołączyć w nagłówku programu.
Wskazane jest, aby w przypadku własnych funkcji postępować podobnie. Przy pisaniu kolejnych programów
wystarczy wówczas włączyć, za pomocą dyrektywy # include, gotowy plik nagłówkowy z potrzebnymi funk-
cjami.
Przykład 1
#include <stdio.h>
#include <stdlib.h>
#include "MojeFunkcje"
Występujące w powyższym przykładzie, w dwóch pierwszych wierszach, pliki nagłówkowe dotyczą języka C
i mogą być wykorzystywane również przy programowaniu w C ++ . Użycie symboli < oraz > oznacza, że nazwy
tych plików odnoszą sie do standardowego pliku umieszczonego w katalogu standardowym. Natomiast symbole
” oznaczają plik założony przez użytkownika i umieszczony domyślnie w katalogu bieżącym, od którego zaczyna
się jego poszukiwanie. Poniższe przykłady pokazują sposoby ich wykorzystania.
Przykład 2
#include <iostream>
void czytaj();
void sortuj();
void pisz();

int main()
{
czytaj();
sortuj();
pisz();
return 0;
}

15
void czytaj() { std::cout << "czytaj()\n"; }
void sortuj() { std::cout << "sortuj()\n"; }
void pisz() { std::cout << "pisz()\n"; }
Zauważmy, że funkcje są zadeklarowane przed ich wywołaniem w programie głównym, natomiast ich definicje
mogą być umieszczone w dowolnym miejscu, np. na końcu programu. W kolejnym przykładzie definicje funkcji
zostały umieszczone we własnej bibliotece, dołączanej dyrektywą # include.
Przykład 3
#include "MojeFunkcje"

int main()
{
czytaj();
sortuj();
pisz();
return 0;
}
gdzie biblioteka MojeFunkcje jest następująca
#include <iostream>
using namespace std;
//nie trzeba poprzedzac cin cout specyfikatorem std::

void czytaj() { cout << "czytaj()\n"; }


void sortuj() { cout << "sortuj()\n"; }
void pisz() { cout << "pisz()\n"; }
W powyższych przykładach funkcje nie mają ani argumentów, ani wartości, a ich treść (body) w nawiasach
klamrowych zawiera tylko jedną instrukcję. Do bardziej złożonych funkcji powrócimy po omówieniu podstaw
języka C ++ .
Nazwa cout (console output) występująca w treści funkcji i oznaczająca wyprowadzanie danych na standar-
dowe wyjście (monitor) jest pobierana z biblioteki iostream. Aby uniknąć konfliktu nazw, tj. powtórzenia takiej
samej nazwy w innych plikach bibliotecznych, jest ona umieszczona w przestrzeni nazw (namespace o nazwie
std, będącej jakby podkatalogiem iostream. Jeśli przestrzeń nazw nie jest zadeklarowana dyrektywą using, to
każdorazowe użycie nazwy cout wymaga poprzedzenia nazwą przestrzeni, oddzielonej podwójnym dwukropkiem
::, będącego operatorem zakresu. Podkreślmy, że operacje wejścia/wyjścia nie należą do standardu języka
C ++ . Biblioteki należą do klas, które w tym języku można tworzyć i które w skrócie omówimy pod koniec
skryptu.
Warto zapamiętać, że operator zakresu służy nie tylko do tworzenia kwalifikatora zakresu w rodzaju std::.
Umożliwia on przede wszystkim użycie każdej zmiennej globalnej, czyli zadeklarowanej przed dowolną funkcją
(blokiem) dla uniknięcia konfliktu ze zminną lokalną (wewnątrz bloku) o tej samej nazwie. Poprzedzenie
nazwy zmiennej podwójnym dwukropkiem oznacza zmienną globalną. Zapisujemy więc np. ::k=1 lub k=1, jesli
przypisujemy wartość 1 zmiennej globalnej lub, w drugim przypadku, lokalnej.
Ćwiczenie
Dlaczego w bibliotece MojeFunkcje można było pominąć kwalifikator zakresu std::? Utwórz własną bibliotekę
funkcji (np. imie(), nazwisko(), adres()) i napisz program z ich wykorzystaniem.

5.3 Typy zmiennych


Najczęściej definiowanymi wielkościami są zmienne, które oznaczają zadaną symboliczną nazwę pewnego
obszaru pamięci. W tym obszarze można przechowywać wartości interpretowane zgodnie z zadeklarowanym
typem zmiennej. Wymieniony obszar jest w języku C ++ bardzo ogólnym pojęciem: może nie posiadać nazwy,
może mieć jedną lub kilka nazw a także adres początku obszaru pamięci może być dostępny dla programisty.

16
Zmienne muszą być zdefiniowane przez podanie jednego z typów, np. char, short, int, long, float, do-
uble, aby określić wielkość obszaru pamięci dla danej zmiennej. Zmienna typu char (znak - character) zajmuje
jedno słowo pamięci (1 bajt), każdy z kolejnych typów oznacza odpowiednio więcej słów zarezerwowanych dla
zadeklarowanej zmiennej. Zajmowany przez daną zmienną obszar pamięci można określić za pomocą operatora
sizeof, np. sizeof(char)=1. Zajętość pamięci przez inne typy zmiennych można uzyskać wykorzystując przy-
kład 04_7_str113.cpp w [3]. Można deklarować również wartości funkcji. Niezadeklarowana wartość funkcji
jest przyjmowana domyślnie jako int. Jest również możliwość zadeklarowania wartości funkcji jako typ void,
oznaczająca, że zbiór wartości jest pusty (funkcja nie posiada żadnej wartości).
Przykłady
char litera = 'A';
char znak;
char *nazwa = &znak;
const double pi = 3.1415926;
enum day {Mon, Tue, Wed, Thu, Fri, Sat, San};
extern int i;
Słowo kluczowe const przekształca zmienną pi w stałą symboliczną, która pozostaje niezmienną w progra-
mie. Natomiast słowo kluczowe enum definiuje siedem stałych całkowitych, przypisując im wartości od 0 do 6
oraz nowy typ day. W ostatniej linii jest użyta tzw. deklaracja referencyjna extern, której musi towarzyszyć
odpowiednia definicja, znajdująca się w innym pliku, np. bibliotecznym.
Gwiazdka w 3-cim wierszu oznacza adres początku obszaru pamięci (wskaźnik). Deklaracja z gwiazdką jest
rozumiana następująco: nazwa jest wskaźnikiem obszaru pamięci zmiennej typu char. Operator & (ampersand)
udostępnia adres obiektu stojącego po jego prawej stronie, którym w naszym przekładzie jest wcześniej zade-
klarowany znak. Deklaracje drugą i trzecią w powyższych przykładach można zapisać w sposób równoważny:
char znak;
char *nazwa;
nazwa = &znak;
Zapis ten mówi, że deklarację z gwiazdką należy odczytywać następująco: nazwa jest wskaźnikiem obszaru
pamięci zmiennej typu char.

5.4 Słowa kluczowe


Do najważniejszych słów kluczowych, z których programista może korzystać jedynie zgodnie z ich znaczeniem
są, oprócz typów zmiennych, w kolejności alfabetycznej następujące symbole:
asm, auto,
break,
case, catch, class, const, continue,
default, delete, do,
else, enum, extern,
for, friend,
goto,
if, inline,
new,
operator,
private, protected, public,
register, return,
signed, sizeof, static, struc, switch,
template, this, throw, try, typedef,
union, unsigned,
virtual, volatile,
while.
Jak łatwo zauważyć, są to słowa angielskie, których znaczenie w programowaniu może być zrozumiałe nawet
bez szczegółowego opisu i łatwe do zapamiętania.

17
5.5 Operatory
5.5.1 Operator przypisania
Jest to najczęściej używany operator umożliwiający wprowadzenie do obszaru pamięci opisanej jakąś zmienną
wartości określonego wyrażenia. Operator oznaczany "="powoduje przypisanie argumentowi po jego lewej stro-
nie wartości wyrażenia po jego prawej stronie.
Przykład 1
suma = suma + t[i]; \\ nowa wartosc sumy powiekszona o i-ty element tablicy t
Przykład 2
main()
{
int i, j;
i = j = 0; \\ kazdej zmiennej przypisano 0
}

5.5.2 Operatory arytmetyczne

Tablica 1: Operatory arytmetyczne

Operator Operacja Zastosowanie


* mnożenie wyrażenie*wyrażenie
/ dzielenie wyrażenie/wyrażenie
% dzielenie modulo (reszta) wyrażenie%wyrażenie
+ dodawanie wyrażenie+wyrażenie
- odejmowanie wyrażenie-wyrażenie

Przykład
main()
{ const float pi=3.141592;
float c, r, A, a, b, h;
c = 2*pi*r; \\ wyrazenie na obwod okregu
A = (a+b)/2*h; \\ wyrażenie na pole trapezu
}

5.5.3 Operatory logiczne, porównania oraz relacji

Tablica 2: Operatory logiczne, porównania oraz relacji

Operator Operacja Zastosowanie


! negacja logiczna !wyrażenie
< mniejsze wyrażenie<wyrażenie
<= mniejsze lub równe wyrażenie<=wyrażenie
> większe wyrażenie>wyrażenie
>= większe lub równe wyrażenie>=wyrażenie
== równe wyrażenie==wyrażenie
!= nierówne wyrażenie!=wyrażenie
&& iloczyn logiczny wyrażenie&&wyrażenie
|| suma logiczna wyrażenie||wyrażenie

18
5.5.4 Operatory zwiększania i zmniejszania
Operatory zwiększania "++"i zmniejszania --"umożliwiają dodanie i odjęcie od danej wartości liczby 1 i
mogą występować jako przedrostek lub przyrostek zmiennej.

5.5.5 Operatory bitowe

Tablica 3: Operatory bitowe

Operator Operacja Zastosowanie


˜ negacja bitowa ˜wyrażenie
« przesuwanie w lewo wyrażenie«wyrażenie
» przesuwanie w prawo wyrażenie»wyrażenie
& koniunkcja bitowa wyrażenie&wyrażenie
ˆ różnica symetryczna wyrażenieˆwyrażenie
| alernatywa bitowa wyrażenie|wyrażenie

Przykład
unsigned char wynik;
unsigned char b1=0145; \\ 01010001
unsigned char b2=0229; \\ 01100101
wynik = b1 & b2; \\ 01000001
Zauważmy, że operatory przesunięcia « oraz » występowały już, jako zapożyczone, w klasach opisujących
funkcje wejścia/wyjścia.

5.5.6 Reguły pierwszeństwa


Tak jak w zapisie wzorów matematycznych, również w C ++ można korzystać z reguł pierwszeństwa operacji,
co pozwala zmniejszyć liczbę nawiasów i poprawić przejrzystość wyrażeń. Każda operacja ma określony poziom
pierwszeństwa i kierunek łączności. Np. operacje mnożenia i dzielenia arytmetycznego mają poziom 13L, a
dodawania i odejmowania 12L, gdzie L oznacza łączność od lewej ku prawej.

19
6 Programowanie w C ++
6.1 Instrukcje
Instrukcja (statement) to ciąg wyrażeń (expression) i słów kodowych zakończony średnikiem, ;. Taki ciąg
jest odpowiednikiem zdania w języku naturalnym i musi być zgodny ze składnią (syntaksą) języka C ++ . Zakoń-
czenie instrukcji średnikiem zamiast kropką wynika z tego, że kropka została zarezerwowana dla oznaczenia
elementu struktury lub klasy, o których nieco więcej powiemy w rozdziale 7.
Z przykładami instrukcji mieliśmy do czynienia przy omawianiu elementarza języka. Obecnie przystąpimy
do opisu składni (syntax) podstawowych instrukcji w C ++ .

6.1.1 if
Składnia instrukcji warunkowej if jest następująca:
if ( wyrazenie )
instrukcja1;
W powyższej instrukcji sprawdzany jest warunek występujący w nawiasach okrągłych po słowie kluczowym
if. Gdy warunek jest spełniony wykonuje się czynność opisaną słowem "instrukcja1".
Przykład
if ( min > t[i] )
min = t[i];
W przypadku gdy warunek nie jest spełniony i ma być wykonana instrukcja opisana przez "wyrazenie2", to
możemy skorzystać z instrukcji if o następującej składni:
if ( wyrazenie )
instrukcja1;
else
instrukcja2;

6.1.2 switch
Instrukcją ze słowem kluczowym switch posługujemy się, jeśli mamy do czynienia z kilku wzajemnie wy-
kluczającymi się warunkami. W tej instrukcji warunek logiczny występuje jako etykieta po słowie case.
Przykład
char znak;
int aLicz=eLicz=iLicz=oLicz=uLicz=yLicz=0;
switch ( znak )
{
case a:
++aLicz;
break;
case e:
++eLicz;
break;
case i:
++iLicz;
break;
case o:
++oLicz;
break;
case u:
++uLicz;
break;

20
case y:
++yLicz;
break;
}
Słowo kluczowe break jest konieczne; powoduje zakończenie instrukcji switch dla znalezionego przypadku.
Ćwiczenie
Co należałoby dopisać w powyższym przykładzie, aby zliczane były również samogłoski pisane dużą literą?

6.1.3 while
Składnia instrukcji while jest następująca:
while ( wyrazenie )
instrukcja;
Gdy "wyrazenie"jest prawdziwe to "instrukcja"jest wykonywana.
Przykład
i = nSilnia = 1;
while ( i <= n )
{
nSilnia = nSilnia * i;
i++;
}

6.1.4 for
Składnia instrukcji for jest następująca:
for ( instrukcja-ini; wyrazenie1; wyrazenie2 )
instrukcja;
przy czym "instrukcja-ini"może być deklaracją albo wyrażeniem. Pętla opisana instrukcją for jest wyko-
nywana dla tylu iteracji, dla ilu "wyrazenie1" okazuje się być prawdziwe. Po każdej iteracji pętli oblicza się
"wyrazenie2", służące głównie do aktualizacji tych zmiennych, którym wartości początkowe nadała "instrukcja-
ini".
Przykład
const int rzm = 24;
int ti[rzm];

for ( int i=0; i < rzm; i++ )


ti[i] = i;

6.1.5 do
Składnia instrukcji do jest następująca:
do
instrukcja;
while ( wyrazenie );
Instrukcja ta może również służyć do opisu pętli, ale w przeciwieństwie do wcześniejszych wersji, najpierw
jest wykonywana "instrukcja" a następnie badany warunek opisany przez "wyrażenie".
Przykład

21
i = nSilnia = 1;
do
{
nSilnia = nSilnia * i;
i++;
}
while ( i < n ):

6.1.6 break
Instrukcja break występowała już w przykładzie ilustrującym działanie instrukcji warunkowej switch i
kończącej sprawdzanie dalszych warunków, jeśli któryś z nich został już spełniony. Taką samą rolę odgrywa
break w obliczeniach w pętli opisywanej którąś z instrukcji while, do lub for.
Przykład
const int rzm = 24;
float wrt, ti[rzm];
int indeks = -1;

for ( int i=0; i < rzm; i++ )


if ( wrt == ti[i] )
{
indeks = i;
break;
}

6.1.7 continue
Instrukcja continue powoduje zakończenie bieżącej iteracji w pętli opisywanej jedną z instrukcji while, do
lub for. W przypadku while i do przystępuje się do wykonywania instrukcji sterującej pętlą. W przypadku for
oblicza się ponownie "wyrazenie2". W przeciwieństwie do breake, która kończy wykonywanie pętli, continue
kończy jedynie bieżącą iterację.

6.1.8 goto
Składnia goto jest następująca
goto etykieta;
przy czym zarówno instrukcja jak i " etykieta" muszą występować w tej samej funkcji. Ze względu na to,
że użycie goto powoduje przerwanie linearnego sposobu odczytu z pamięci, korzystanie z tej instrukcji nie jest
zalecane.

6.2 Funkcje
Wspominaliśmy już, jak ważną sprawą jest styl programowania, polegający na odpowiednim wykorzystaniu
pojęcia funkcji. Tak napisany program ułatwia jego zrozumienie i może znacznie zyskać na przejrzystości.
Przystępując więc do programowania warto zastanowić się nad nazwami zadań, które mamy rozwiązać.
Wskazane jest użycie takich nazw w tworzonym algorytmie. Jak już wspomnieliśmy, nawet jeśli algorytm jest
w formie tekstowej i zawiera pewne elementy języka programowania, to jest bliższy językowi naturalnemu.
Programowanie w dobrym stylu przyporządkowuje zadaniom z algorytmu funkcje o takich samych nazwach.
Warto pamiętać, że trafna nazwa jest już połową sukcesu w poprawnym programowaniu.

22
6.2.1 Definiowanie funkcji
Funkcja jest więc w programie reprezentowana przez swoją nazwę. Definicję argumentów funkcji umieszcza
się w jej liście argumentów ujętej w parę nawiasów okrągłych, a poszczególne argumenty rozdziela się prze-
cinkami. Nadawanie argumentom, które występują w jej definicji, nazw nie jest obligatoryjne, ale wskazane dla
lepszego rozumienia działania funkcji. Takie argumenty nazywamy formalnymi. Zbiór argumentów może być
pusty (void). Przy definiowaniu argumentu można mu nadać tzw, wartość domniemaną (wyrażenie domnie-
mane). Argumenty domniemane muszą znajdować się na końcu ich listy. O argumentach powiemy jeszcze
więcej w sekcji o wywoływaniu funkcji.
Wróćmy jeszcze do funkcji o zastrzeżonej nazwie main(). Może ona posiadać argumenty zapisane nastę-
pująco: main(int ac, char *av[]), gdzie ac (argument counter ) jest licznikiem argumentów, a av (argument
vector ) jest tablicą tych argumentów. Definicja tablicy char *av[] jest odczytywana następująco: av jest tablicą
wskaźników do (ciągów) znaków. Argumenty te wypisujemy za nazwą skompilowanego programu. Ułatwia to
użycie programu, bo wywołuje się go tak, jakby był komendą systemu operacyjnego.
Przykład 1
#include <iostream>
using namespace std;
#include <cstdlib> //dla funkcji atof

int main(int ac, char *av[])

{
cout << "Wydruk argumentow av" << endl;

for (int i=0; i<ac; i++)


{
cout << "Argument nr " << i << " to C-string: " << av[i] << endl;
}

float t;
t = atof(av[2]);
cout.precision(3);
cout.width(6);
cout << "Temperatura podwyzszona: " << t+5.1 << endl;

}
Po skompilowaniu programu, np. w systemie Linux, jego uruchomienie
./argMain temperatura 36
daje następujący wydruk:
Wydruk argumentow av
Argument nr 0 to C-string: ./argMain
Argument nr 1 to C-string: temperatura
Argument nr 2 to C-string: 36
Temperatura podwyzszona: 41.1
Wynikiem funkcji jest jej wartość zgodna z zadeklarowanym typem. Jak już wspomnieliśmy, zadeklarowana
wartość funkcji może być również typu void, co oznacza, że funkcja nie przekazuje żadnej wartości. Czynność
wykonywana przez funkcję jest ujęta w jej treści (body), ujętej w parę nawiasów klamrowych.
Przykład 2
inline int abs( int i ) // obliczanie wartosci bezwzglednej i
{
return( i < 0 ? -i : i );
}

23
Zastąpienie pierwszego wiersza w powyższym przykładzie linią
inline int abs( int i = 1 ) // obliczanie wartosci bezwzglednej i
{
...
}
oznacza nadanie argumentowi wartości domniemanej i=1.
Przykład 3
inline int min( int l1, int l2 ) // znajdywanie mniejszej z dwóch liczb
{
return( l1 < l2 ? l1 : l2 );
}
W przykładach 1 i 2 został użyty symbol "?". Jest to tzw. operator wyrażenia warunkowego. Jest to
jedyny operator trójargumentowy o składni:
a ? b : c
Jego działanie jest następujące. Sprawdzana jest wartość logiczna wyrażenia " a", jeśli jest ono prawdziwe,
to wynikiem operacji jest wartość "b", a jeśli nieprawdziwe to " c".
Przykład 4
nwd( int l1, l2 ) // znajdywanie najwiekszego wspolnego dzielnika
{
int wd
while (l2)
{
wd = l2;
l2 = l1 % l2;
l1 = wd;
}
return( l1 );
}
Ćwiczenie
W przykładzie 1 wyprowadzanie na wyjście standardowe zastąp wpisywaniem do pliku. Nazwa pliku ma być
kolejnym argumentem skompilowanego programu. Potrzebne informacje znajdziesz w sekcji 6.2.4.

6.2.2 Rekurencja
Funkcję, która bezpośrednio lub pośrednio wywołuje samą siebie, nazywamy funkcją rekurencyjną.
Przykład 1
rnwd( int l1, l2 ) // znajdywanie najwiekszego wspolnego dzielnika
{
if (l2 == 0)
{
return l1;
return rnwd( l2, l1 % l2);
}
return( l1 );
}
Kolejne iteracje dla funkcji z przykładu 1 są przedstawione w tabeli 4.

Przykład 2

24
Tablica 4: Iteracje przy obliczaniu rnwd(15,123)

l1 l2 wynik
15 123 rnwd(123,15)
123 15 rnwd(15,3)
15 3 rnwd(3,0)
3 0 3

unsigned long silnia( int n )


{
if (n > 1)
return n = n * silnia( n-1 );
return( 1 );
}
Ćwiczenie
Co się stanie, jeśli warunek zakończenia dla funkcji w przykładzie 2 będzie następujący?
if (n != 0)

6.2.3 Wywoływanie funkcji


Obliczanie funkcji w wybranym miejscu programu dokonywane jest po umieszczeniu jej nazwy w tym miejscu
razem z operatorem wywołania, czyli parą nawiasów okrągłych "()". W operatorze wywołania należy umieścić
argumenty aktualne (argumenty wywołania), które tak jak w definicji funkcji należy rozdzielić przecinkami.
Nazywamy to przekazaniem argumentów (passing arguments) do funkcji. Argumenty, które przy definiowa-
niu funkcji były podane jako domniemane można przywoływaniu pominąć, co oznacza nadanie im wartości
domniemanych.
Po zakończeniu obliczeń związanych z wykonywaną funkcją, argumenty aktualne mają wartość taką, jak
przed wywołaniem funkcji, nawet jeśli w trakcie działania funkcji były zmieniane. Wynika to z przechowywania
wartości zmiennej w trakcie wykonywania funkcji jedynie na stosie (pamięci podręcznej). Mówimy wówczas
o wywoływaniu przez wartość. Jeśli chcemy zmienić wartość jednego tylko argumentu, można zwrócić jego
aktualną zawartość do wartości funkcji i ją podstawić pod argument. Zmiana wartości większej liczby argu-
mentów jest możliwa dzięki wykorzystaniu operatora &, który jak wiadomo oznacza adres zmiennej. Mówimy
wówczas, że przesyłamy argumenty przez referencję. Poniższy przykład ilustruje wszystkie 3 przypadki. Przy
przesyłaniu przez referencję wykorzystaliśmy dodatkowo wskaźnik, oznaczony gwiazdką *.
Przykład
#include <cstdlib>
using namespace std;
//nie trzeba poprzedzac cin cout specyfikatorem std::
#include <iostream>
//biblioteka dla strumienia na standardowe wejscie/wyjscie

funkcja(int Z1, int Z2, int *wskZ3)


// wartosc funkcji domyslnie typu int
{
Z1=Z2=*wskZ3=100;
return Z2;
}
main()

{
int z1=-1;
int z2=-1;

25
int z3=-1;

cout << "Wartosci poczatkowe zmiennych \t z1, z2, z3 "


<< '\t' << z1 << '\t' << z2 << '\t' << z3 << '\n';

z2=funkcja(z1,z2,&z3);

cout << "Wartosci koncowe zmiennych \t z1, z2, z3 "


<< '\t' << z1 << '\t' << z2 << '\t' << z3 << '\n' << '\a';

}
W powyższym przykładzie ujęte apostrofami są znaki specjalne:
0
\ a0 - sygnał dzwiękowy (Alarm),
0
\ n0 - oznacza nową linię (New line),
0
\ b0 - cofacz (Backspace),
0
\ f 0 - nowa strona (Form feed),
0
\ r0 - powrót karetki (carriage Return),
0
\ t0 - tabulator poziomy (Tabulator),
0
\ v 0 - tabulator pionowy (Vertical tabulator).
Ćwiczenie
W podanym przykładzie przećwicz użycie innych, wymienionych powyżej, znaków specjalnych.
Sposób wywołania funkcji zależy od jej zadeklarowania. Jeżeli funkcja jest zadeklarowana jako inline, jak to
ma miejsce w przykładach 1 oraz 2 w sekcji 6.2.1, to podczas jej kompilacji programu treść funkcji będzie rozwi-
nięta w miejscu jej wywołania. W przeciwnym razie funkcja będzie wywołana podczas wykonywania programu,
co powoduje wspomnianą już wcześniej nielinearność odczytywania zawartości pamięci. Użycie inline pozwala
na oszczędność pamięci (nie trzeba kopiować i przechowywać argumentów) i przyspiesza obliczenia. Jednak nie
wszystkie funkcje dają się rozwinąć, jak to ma miejsce dla funkcji rekurencyjnej w przykładzie 3. Mechanizm
inline jest najbardziej przydatny dla funkcji małych, prostych i często wywoływanych.

6.2.4 Funkcje wejścia/wyjścia


Jak już wspomnieliśmy, środowisko z którego korzysta programista, ma najczęściej wiele gotowych do użycia
standardowych funkcji bibliotecznych (stdlib) lub służących do komunikowania z terminalem (stdio). Poniższy
przykład pokazuje wykorzystanie bibliotek specyficznych dla C ++ , dzięki którym kierowanie stringu (łańcu-
cha znaków) ze standardowego wejścia lub na standardowe wyjście przy użyciu symboli « lub » jest proste i
intuicyjne. Plik nagłówkowy time.h zawiera opisy funkcji losowych wykorzystywanych w programie.
Przykład 1
#include <cstdlib>
using namespace std;
//nie trzeba poprzedzac cin cout specyfikatorem std::
#include <iostream>
//biblioteka dla strumienia na standardowe wejscie/wyjscie
#include <time.h>
// dla funkcji time()

main()

26
int i, j, n, m;
float re;
string s;

cout << "Wprowadz po sobie liczbe wierszy i kolumn macierzy w oddzielnych liniach..\n";
// strumien (ciag znakow) wyprowadzany na standardowe wyjscie (ekran)
cin >> n >> m;
// strumien (ciag znakow) wprowadzany ze standardowego wejscia (klawiatura - console)

s = "n="; cout << s; cout << n;


s = " m="; cout << s; cout << m;
cout << '\n';
//string (znaki) lub liczby wyprowadzane do strumienia

for (i=1; i<=n; ++i)


{
for (j=1; j<=m; ++j)
{
re = ((rand() % 200)-100)/ 10.0;
cout.width(6);
cout << re;
if (j!=m) cout << " ";
}
cout << '\n';
}

return 0;
}
W powyższym przykładzie pod obiekt s klasy string, podstawiany jest ciąg znaków oznaczony cudzysłowami.
Operator = wstawia ten ciąg znaków do s, natomiast operatorem += można dopisać do s kolejne ciągi znaków.
Najważniejsze pojęcia o obiektach i klasach będą jeszcze podane w rozdziale o podwyższeniu poziomu języka.
Zmienne, które są liczbami, przesuwane są na standardowe wyjście bez dodatkowych oznaczeń.
Użyta została również funkcja width w celu określenia jednego z parametrów formatowania jakim jest liczba
znaków przypisana danemu stringowi lub liczbie. Inne przykłady funkcji umożliwiających formatowanie to np.
precision, fill.
Poniższy przykład ilustruje użycie funkcji rand() w symulowaniu 20-tu rzutów kostką do gry.
Przykład 2
#include <cstddef>
#include <cstdlib>
#include <iostream>
#include <ctime>

int main()
{
std::srand(std::time(0));
// std::srand(std::time(nullptr));
// use current time as seed for random generator
int random_variable = std::rand();
std::cout << "Random value on [0 " << RAND_MAX << "]: "
<< random_variable << '\n';

// roll a 6-sided die 20 times


for (int n=0; n != 20; ++n) {
int x = 7;

27
while(x > 6)
x = 1 + std::rand()/((RAND_MAX + 1u)/6); // Note: 1+rand()%6 is biased
std::cout << x << ' ';
}
std::cout << '\n';
}
Kolejny przykład pokazuje, jak można standardowe wyjście zastąpić plikiem wyjściowym. Pojawia się dodat-
kowa deklaracja strumienia o nazwie los. Plik o nazwie wprowadzonej ze standardowego wejścia jest otwierany w
trybie ate (at end) oznaczającym otwarcie i ustawienie się na końcu zawartości. Po wprowadzeniu określonych
stringów i danych liczbowych należy pamiętać o zamknięciu pliku. Inne możliwe tryby otwierania są następu-
jące: in (input) - otwieranie pliku do czytania, out (output) - otwieranie pliku do pisania, app (append) -
otwieranie do dopisywania na końcu pliku, trunc (truncate) - otwieranie i jeśli plik istnieje, skasowanie starej
treści, binary - tryb ma być binarny (domniemany jest tekstowy).
Przykład 3
#include <cstdlib>
using namespace std;
//nie trzeba poprzedzac cin cout specyfikatorem std::
#include <iostream>
//biblioteka dla strumienia na standardowe wejscie/wyjscie
#include <fstream>
//biblioteka dla klas ofstream, ifstream, fstream (oba wczesniejsze) -
//pochodne klas ostream, istream
#include <time.h>
// dla funkcji time()

main()

{
int i, j, n, m;
float re;
string plikA;
string s;
ofstream los;
//otput file stream (deklaracja strumnienia wyprowadzanego do pliku)

cout << "Wprowadz nazwe pliku wynikowego..\n";


// string (ciag znakow) wyprowadzany na standardowe wyjscie (ekran)
cin >> plikA;
los.open(plikA.c_str(),ios::ate);

cout << "Wprowadz po sobie liczbe wierszy i kolumn macierzy w oddzielnych liniach..\n";
// strumien (ciag znakow) wyprowadzany na standardowe wyjscie (ekran)
cin >> n >> m;
// strumien (ciag znakow) wprowadzany ze standardowego wejscia (klawiatura - console)

s = "n="; los << s; los << n;


s = " m="; los << s; los << m;
los << '\n';
//string (znaki) lub liczby wyprowadzane do strumienia

for (i=1; i<=n; ++i)


{
for (j=1; j<=m; ++j)
{

28
re = ((rand() % 200)-100)/ 10.0;
los.width(6);
los << re;
if (j!=m) los << " ";
}
los << '\n';
}
los.close();

return 0;
}
Nazwa pliku jest przechowywana w obiekcie klasy string, tak jak to jest zadeklarowane na początku pro-
gramu. Wykorzystywana funkcja open oczekuje jednak C-stringu, co wymaga posłużenia się funkcją składową
c_str(), w sposób pokazany w przykładzie.
Podobnie jak standardowe wyjście, również standardowe wejście można zastąpić plikiem. Klasę ofstream
należy jedynie zastąpić przez ifstream.
Poznaliśmy operatory = oraz +=, które umożliwiają wpisywanie znaków w obiektach klasy string. Aby
wpisywać liczby, należy korzystać z obiektów klasy ostringstream, jak to jest pokazane w poniższym przykła-
dzie:
Przykład 4
#include <cstdlib>
using namespace std;
//nie trzeba poprzedzac cin cout specyfikatorem std::
#include <iostream>
//biblioteka dla strumienia na standardowe wejscie/wyjscie
#include <fstream>
//biblioteka dla klas ofstream, ifstream, fstream (oba wczesniejsze) -
//pochodne klas ostream, istream
#include <sstream>
//bibliteka dla klasy ostringstream
#include <iomanip>
//dla manipulatora setw
#include <math.h>
// dla funkcji sin

main()

{
float a, b, d, x, y;
string plikA;
//obiekty klas sting (s) oraz ostringstream (ss);
string s;
ostringstream ss;
ofstream los;
//otput file stream (deklaracja strumnienia wyprowadzanego do pliku los)

cout << "Wprowadz nazwe pliku wynikowego..\n";


// string (ciag znakow) wyprowadzany na standardowe wyjscie (ekran)
cin >> plikA;
los.open(plikA.c_str(),ios::ate);

cout << "Wprowadz po sobie w oddzielnych liniach: poczatek i koniec przedzialu oraz krok \n";
// strumien (ciag znakow) wyprowadzany na standardowe wyjscie (ekran)
cin >> a >> b >> d;

29
// strumien (ciag znakow) wprowadzany ze standardowego wejscia (klawiatura - console)
ss << "przedzial funkcji:\n"
<< fixed << setprecision(2)
// for inteager use setw(2)
<< " poczatek=\t" << a << '\n'
<< " koniec=\t" << b << '\n'
<< " krok=\t\t" << d << '\n';
los << ss.str();
los << "!!!wprowadzanie liczb do obiektu klasy ostrinstream!!!\n";
s = "przedzial funkcji:\n";
s += " poczatek="; s += a; s += '\n';
s += " koniec="; s += b; s += '\n';
s += " krok="; s += d; s += '\n';
los << s;
// !!!operator += nie dopisuje liczb do stringu!!!
los << "!!!operator += nie dopisuje liczb do stringu!!!\n";
los << "przedzial funkcji:\n" << " poczatek=" << a << '\n'
<< " koniec=" << b << '\n' << " krok=" << d << endl;
// przesuwanie stringow i liczb bezposrednio do pliku
los << "liczby wpisywane bezposrednio do pliku\n";

los << " x\t" << "sin(x)\n";


x = a;
while ( x < b )
{
y = sin( x );
los.width(6);
los << x << '\t' << y << endl;
x+=d;
//operator += dziala rowniez w klasie string!
//x=x+d;
}

los.close();

return 0;
}
Można pobierać liczby ze strumienia posługując się klasą istringstream, podobnie jak przy wpisywaniu
liczb do strumienia pomocna była, w powyższym przykładzie, klasa ostringstream. Powrócimy jeszcze do
tych klas w sekcji 7.4, a zainteresowanych zachęcam do lektury [2].
Ćwiczenie 1
W przykładzie 1 przećwicz użycie innych znaków specjalnych (sekcja 6.2.3) i funkcji formatowania.
Ćwiczenie 2
Zmień program w przykładzie 2 tak, aby liczba oczek była wypisywana nie na wyjściu standardowym ale w
pliku.
Ćwiczenie 3
Zmodyfikuj program w przykładzie 2 tak, aby np. po 100 seriach, każda np. po 24 rzuty, otrzymać częstość
występowania poszczególnych liczb oczek. Wskazówka: częstość należy rozumieć jako liczbę oczek podzieloną
przez liczbę serii.
Ćwiczenie 4
Wykorzystując przykład 3, utwórz tablice innych funkcji np. exp(x), pozostałych trygonometrycznych
cos(x), tg(x), ctg(x), oraz hiperbolicznych sinh(x), cosh(x), tgh(x), ctgh(x). Jak dobierać wartości po-
czątkową i końcową przedziału tablicowania dla funkcji okresowych, aby otrzymać wartości funkcji w wybranej
ćwiartce okresu?

30
Ćwiczenie 5
W przykładzie 4 zastąp bezpośrednie zapisywanie do pliku zapisywaniem najpierw do obiektu ss, a następnie
przesuń ss do pliku.
Ćwiczenie 6
Wzorując się na przykładzie z ćwiczenia 4 stablicować funkcje zdefiniowane w 6.2.1 i 6.2.2.
Ćwiczenie 7
Zmień program w przykładzie 3 tak, aby liczba wierszy i kolumn była wczytywana z pliku danych, a nie
z wejścia standardowego. Skorzystaj z klasy ifstream, analogicznie jak to jest pokazane w przykładzie dla
ofstream.
Ćwiczenie 8
Przykład 4 zmienić tak, aby dla zadanego w pliku danych przedziału i kroku, utworzyć w pliku wyjściowym
tablicę wybranej funkcji standardowej (np. trygonometrycznej).

31
7 Podwyższenie poziomu w języku C ++
7.1 Dynamiczny przydział pamięci
Pojęcie dynamicznego przydziału (alokacji) pamięci wiąże się ze sposobem linearnego odczytu/zapisu pamięci
procesora. Przy takim korzystaniu z pamięci mówimy, że zmienne przechowywane są na wspomnianym przy
omawianiu funkcji stosie (stack) i potrzebny rozmiar pamięci musi być z góry zadeklarowany, niezależnie od
tego jakie będzie jej wykorzystanie w trakcie obliczeń. Aby wykorzystanie pamięci było lepsze, kosztem czasu
obliczeń wprowadza się inny sposób jej przydziału, jakim jest alokacja na stercie (heap). Sterta to obszar
pamięci, w którym możemy sami rezerwować miejsce i je zwalniać w dowolnym momencie działania programu.
W dynamicznym przydziale pamięci deklarujemy jedynie wskaźnik do zmiennej oznaczony gwiazdką (∗),
która występuje przed nazwą zmiennej. Wskaźnikiem jest adres zmiennej, a jeśli zmienną jest tablica, adres
jej pierwszego elementu. Do wskaźnika przydzielamy potrzebną ilość pamięci, posługując się operatorem new,
podając typ zmiennej oraz liczbę jej elementów. Jeśli pamięć przestaje być potrzebna, to należy ją zwolnić
operatorem delete. Zwalniając tablicę, poprzedzamy jej nazwę prostokątnymi kwadratami, ([ ]).
Przykład
#include <cstdlib>
using namespace std;
//nie trzeba poprzedzac cin cout specyfikatorem std::
#include <iostream>
//biblioteka dla strumienia na standardowe wejscie/wyjscie
#include <fstream>
//biblioteka dla klas ofstream, ifstream, fstream (oba wczesniejsze) -
//pochodne klas ostream, istream

main()

{
ifstream alokacja;
int i=0, j=0;
string s;

alokacja.open("alokacja.txt");
// w pliku alokacja.txt wszystkie dane sa w osobnych wierszach
int *A, suma=0;
A = new int [20];

while(getline(alokacja,s))
{
i++;
A[i] = atoi(s.c_str());
// Ascii TO Integer
if (A[i] > 0)
{
suma = suma + A[i];
j++;
}
}

delete []A;
alokacja.close();

cout << "Jest " << i << " danych. \n";


cout << "Wsrod danych jest " << j << " elementow dodatnich. \n";

32
cout << "Ich suma jest rowna " << suma << ". \n";

}
Ćwiczenie 1
Zmienić program z przykładu tak, aby w jednym przejściu tablicy znajdywane były wartości najmniejsza i
największa.
Ćwiczenie 2
Rozbudować program z przykładu tak, aby nazwa pliku z danymi była wprowadzana przez użytkownika, a
wynik wyprowadzany nie na wyjście standardowe a do pliku.

7.2 Struktura
Struktura to typ danych definiujący grupę zmiennych logicznie ze sobą powiązanych. Struktura jest tworzona
przy użyciu instrukcji struct.
Przykład 1
struct adres {
string ulica;
int numer;
int kod;
string miasto;
}
Przykład 2
#include <cstdlib>
using namespace std;
//nie trzeba poprzedzac cin cout specyfikatorem std::
#include <iostream>
//biblioteka dla strumienia na standardowe wejscie/wyjscie
#include <fstream>
//biblioteka dla klas ofstream, ifstream, fstream (oba wczesniejsze) -
//pochodne klas ostream, istream

struct zawodnik { // definicja struktury zawodnik


string imie;
int wzrost; };

main()

{
int i, n;
zawodnik z[20];
// deklaracja tablicy 20-tu struktur
cout << "Podaj liczbe zawodnikow, \n nie wiecej niz 20-tu. \n ";
cin >> n;

for (i=0; i < n; i++)


{
cout << "Nr " << i+1 << ", podaj swoje imie: ";
cin >> z[i].imie;
cout << " \t Jaki masz wzrost? ";
cin >> z[i].wzrost;
cout << '\n';
}
cout << " \n \t\t Nacisnij klawisz ... \n";

33
while (i >= 0)
{
cout << "Jaki numer wybierasz (ujemny konczy)? ";
cin >> i;
if (i <= 0) break;
i = i-1;
cout << "\n \t " << z[i].imie << " ma wzrost " << z[i].wzrost << "\n\n";
}
}
Za pomocą operatora kropki odwołujemy się do wybranego elementu struktury.
Ćwiczenie 1
Zmień program z przykładu 2 tak, aby lista zawodników była drukowana do pliku.
Ćwiczenie 2
Zmień program z przykładu 2 tak, aby można było nie tylko sprawdzac dane zawodnika ale także korygować.

7.3 Klasa
Klasa jest złożonym typem, tworzonym przy użyciu instrukcji class, zawierającym jako składowe nie tylko
dane, jak to było w strukturze, ale również funkcje operujące na tych danych. Przykładem może być biblioteczna
klasa std::string do operacji z tekstami, z której korzystaliśmy już w poprzedniej sekcji.
Przykład
#include <cstdlib>
using namespace std;
//nie trzeba poprzedzac cin cout specyfikatorem std::
#include <iostream>
//biblioteka dla strumienia na standardowe wejscie/wyjscie
#include <fstream>
//biblioteka dla klas ofstream, ifstream, fstream (oba wczesniejsze) -
//pochodne klas ostream, istream

class zawodnik { // definicja klasy zawodnik


string imie;
int wzrost;
public:
void set ( string i1, int w1)
{
imie = i1;
wzrost = w1;
}
void pokaz()
{
cout << '\n' << imie << " ma " << wzrost << " cm.\n";}
};

main()

{
int i, n, w2;
string i2;
zawodnik z[20];
// deklaracja tablicy 20-tu struktur
cout << "Podaj liczbe zawodnikow, \n nie wiecej niz 20-tu. \n ";
cin >> n;

34
for (i=0; i < n; i++)
{
cout << "Nr " << i+1 << ", podaj swoje imie: ";
cin >> i2;
cout << " \t Jaki masz wzrost? ";
cin >> w2;
z[i].set(i2,w2);
cout << '\n';
}

cout << " \n \t\t Nacisnij klawisz ... \n";


while (i >= 0)
{
cout << "Jaki numer wybierasz (ujemny konczy)? ";
cin >> i;
if (i <= 0) break;
i = i-1;
z[i].pokaz();
}
}
W powyższym przykładzie odwołanie do elementów klasy jest dokonywane przy użyciu operatora kropki,
podobnie jak to miało miejsce w strukturze. W nagłówku programu znajduje się definicja klasy, zawierająca
oprócz dwóch zmiennych (imie, wzrost), również dwie funkcje (set, pokaz). W przeciwieństwie do struktury,
definicja klasy może zawierać nie tylko część prywatną (private), dostępną jedynie dla funkcji klasy, ale również
części publiczną (public) i chronioną (protected). Część publiczna jest dostępna na zewnątrz klasy, a część
chroniona jest dostępna dla tzw. elementów chronionych. Dane umieszczane są najczęściej w części chronionej,
należy raczej unikać umieszczania ich w części publicznej.
Ćwiczenie
Zmień program w przykładzie tak, aby funkcja pokaz działała nie na wyjściu standardowym, ale na pliku.

7.4 Ponownie o operacjach wejścia/wyjścia w C ++


Język C ++ ma właściwe sobie operacje wejścia, wyjścia, jak to już było widoczne we wcześniejszych przy-
kładach. Są one proste w użyciu, jednak ich zrozumienie wymaga znajomości takich pojęć jak klasa i obiekt,
które dopiero teraz zostały w naszym przedmiocie krótko opisane. Operacje te zilustrujemy teraz ponownie,
posługując się poniższym przykładem przedstawiającym przepisywanie ciągu znaków (string) z jednego pliku
do drugiego.
Przykład
#include <iostream>
using namespace std;
//nie trzeba poprzedzac cin cout specyfikatorem std::
#include <fstream>
#include <string>
//************************
int main()
{
string plikA;
string plikB;
//--------------------
cout << " Podaj nazwe pliku wejsciowego: ";
// odpowiednik printf() dla C
cin >> plikA;
// odpowiednik scanf() dla C
ifstream czyt(plikA.c_str());

35
//definicja obiektu klasy ifstream (strumien wejsciowy)
if(!czyt)
{
cout << " Nie moge otworzyc tego pliku";
return 1;
}
//--------------------
cout << " Podaj nazwe pliku wyjsciowego: ";
cin >> plikB;
ofstream pisz(plikB.c_str());
//definicja obiektu klasy ofstream (strumien wyjsciowy)
if(!pisz)
{
cout << " Nie moge otworzyc tego pliku";
return 1;
}
//---przepisywanie---
char c;
do
{
czyt.get(c);
pisz.put(c);
}
while(!czyt.eof());
//----sprawdzanie----
if(czyt.eof())
cout << "\n Przepisano z "
<< plikA
<< " do "
<< plikB
<< ".\n";
else
cout << " Blad czytania\n";
return 0;
}
//***************************
W nagłówku programu włączona została biblioteka umożliwiająca korzystanie z klasy string, tak jak to
było pokazane w sekcji 6.2.4. Przypomnijmy, że bardziej zaawansowane operacje na stringach można wykonywać
korzystając z klas ostringstream oraz istringstream. Konieczne jest wówczas włączenie w nagłówku biblioteki
sstream zawierającej te klasy. Użycie klasy ostringstream było zilustrowane przykładem 3 w sekcji 6.2.4.
Pokazano, że do strumienia ss można przesuwać zarówno pojedyncze znaki, stringi jaki całe liczby równie
prosto jak na standardowe wyjście. Podobną rolę odgrywa klasa istringstream. Jako składowe posiada ona
takie funkcje jak mem (zapamiętywanie strumienia w pamięci podręcznej), find (znajdywanie w strumieniu
zadanego stringu z określeniem jego miejsca w strumieniu), length (obliczanie długości zadanego stringu) oraz
seekg (ustawianie wskaźnika w zadanym miejscu strumienia dla odczytu - get). Analogiczna dla ostatniej
funkcji jest seekp (seek put), która jest składową klasy ostringstream ustawiającą wskaźnik w strumieniu do
pisania. Zadanym stringiem może być np. „temperatura” w przykładzie 1 sekcji 6.2.1 i łatwo sobie wyobrazić, jak
podane funkcje składowe klasy istringstream są pomocne w odczytywaniu wartości tego parametru. Używamy
wówczas operatora przesunięcia „»”, tak jakby to było standardowe wejście cin (analogicznie jak w przykładzie
3 z sekcji 6.2.4 w przypadku wprowadzania liczb do strumienia za pomocą „«”).
Właściwości związane z operacjami na plikach są zasygnalizowane w treści programu krótkimi komentarzami.
Operacje na standardowym wejściu i wyjściu są opisane symbolami odpowiednio „»” oraz „«”, których znaczenie,
jak już wspominaliśmy, łatwo zrozumieć.
W celu czytania ciągu znaków z pliku definiowany jest obiekt klasy ifstream (strumień wejściowy). Temu

36
strumieniowi jest nadawana nazwa „czyt”. Widoczne jest od razu wywołanie konstruktora otwierającego plik
o podanej nazwie. Ta nazwa jest jedynym argumentem konstruktora, drugi argument, jako domniemany, ma
wartość „ios::in”.
Celem zapisu ciągu znaków do pliku definiowany jest obiekt klasy ofstream (strumień wyjściowy). Temu
strumieniowi jest nadawana nazwa „pisz”. Widoczne jest wywołanie konstruktora otwierającego plik o podanej
nazwie. Ta nazwa jest również jedynym argumentem konstruktora, drugi argument, jako domniemany, ma
wartość „ios::out”.
Podobnie jak to miało miejsce we wcześniejszych przykładach, w obu przypadkach operacji wejścia i wyjścia
na plikach ich nazwy są przechowywane w obiektach klasy string, tak jak to jest zadeklarowane na początku
programu. Wykorzystywana funkcja „open” oczekuje jednak „C-stringu”, co jest możliwe po posłużeniu się funkcją
składową „c_str()”.
Ćwiczenie
Zmień powyższy przykład, zastępując czytanie i zapisywanie pojedynczych znaków odczytywaniem i zapi-
sywaniem linii, jak to już było pokazane we wcześniejszych przykładach. Czy oba sposoby mogą prowadzić do
różnych postaci plików wynikowych?

37
8 Programowanie w C ++ na poziomie Formuły 1
Klasy i obiekty, które w ramach klasy mogą być zadeklarowane, są podstawą wspomnianego już progra-
mowania obiektowego orientowanego. Są to jednak zagadnienia, które nie wchodzą raczej w zakres wstępu czy
wprowadzenia do programowania i tej tematyki nie będziemy w ramach naszego kursu rozwijać. Wspomnieliśmy
jedynie o klasach ostream, istream, ostreamstring, istreamstring zawartych odpowiednio w bibliotekach iostream,
sstream. W przykładach pokazaliśmy jakim ogromnym ułatwieniem są one w operacjach na plikach tekstowych.
Chodzi jednak o uświadomienie sobie, że wymienione pojęcia, a także takie jak konstruktor, destruktor, funkcja
zaprzyjaźniona, dziedziczenie, funkcje wirtualne, polimorfizm, umożliwiają dopiero wejście na wysoki poziom
programowania. Na przykład klasa istringstream jest pochodną klasy istream, a zatem dziedziczy wszystkie jej
zachowania (operator przesunięcia », funkcje składowe read, get, itp.). Programowanie na takim poziomie może
być porównane z umiejętnością jazdy w formule 1. W programie studiów tym zagadnieniom jest poświęcony
przedmiot Programowanie obiektowe.

9 Złożoność obliczeniowa algorytmów


Zagadnienia dotyczące złożoności obliczeniowej były pokrótce omówione w sekcji 2. W tej sekcji, w ramach
ćwiczeń i korzystając z nabytej w poprzednich sekcjach umiejętności programowania w C ++ , zajmiemy się
prostymi przykładami ilustrującymi złożoność obliczeniową algorytmów.

9.1 Przykład
W poniższym przykładzie działania mnożenia i dodawania są wykonywane w podwójnej pętli, każda o
rozmiarze N :
//O(N^2) - quadratic complexity, multiplication in 2D loop
#include <iostream>
#include <iomanip>
#include <chrono>
#include <ctime>
#include <thread>
#include<fstream>
#include<string>
using namespace std;

void multipl(int N)
{
double d = 0;
for(int n=0; n<N; ++n)
for(int m=0; m<N; ++m) {
double diff = d*n*m;
d = diff + d;
}
}

int main()
{
int N = 10000;
cout << N << endl;
clock_t c_start = clock();
cout << c_start << endl;

multipl(N);

clock_t c_stop = clock();

38
cout << c_stop << endl;
float comp = 1000 * (c_stop - c_start) / CLOCKS_PER_SEC;
cout << comp << "ms" << endl;

}
Liczba działań zwiększa się więc z kwadratem N , czyli złożoność obliczeniowa jest określona jako O(N 2 ), o
czym można się przekonać zadając różne N . Aby uniknąć kompilacji po każdej zmianie tego parametru, można
go zadać jako argument funkcji main następująco:
//O(N^2) - quadratic complexity, multiplication in 2D loop
#include <iostream>
#include <iomanip>
#include <chrono>
#include <ctime>
#include <thread>
#include<fstream>
#include<string>
using namespace std;

void multipl(int N)
{
double d = 0;
for(int n=0; n<N; ++n)
for(int m=0; m<N; ++m) {
double diff = d*n*m;
d = diff + d;
}
}

int main(int ac, char *av[])


{
int s = 1000;
int N = s * atoi(av[1]);
cout << N << endl;
clock_t c_start = clock();

multipl(N);

clock_t c_stop = clock();


float comp = 1000 * (c_stop - c_start) / CLOCKS_PER_SEC;
ofstream txt("timeB.txt", ios::app);
txt << N << " " << comp << endl;

}
i wywoływać jako komendę z parametrem równym zadanemu N (zwróćmy uwagę, że w programie zadane N
jest zwiększane 1000-krotnie). Wyniki są przekazywane nie jak w poprzednim przykładzie na standardowe wyjście
ale do pliku "timeB.txt". Ostatnim krokiem jest uzyskanie wartości czasu działania programu w określonym
przedziale zmienności N . Można to uzyskać np. przez odpowiedni zapis w języku skryptowym, który dla systemu
operacyjnego Linux jest następujący:
for ((i=0; i<=20; i++)); do
#c++ timeBf.cpp -o tB
./tB $i
done

39
Rysunek 11: Wykres kwadratowej złożoności obliczeniowej algorytmu

40
Po 3-krotnym uruchomieniu programu wsadowego ./tB.sh zawierającego powyższy zapis skryptowy, zapa-
miętaniu wyników w macierzach A, B oraz C i uruchomieniu w środowisku Octave programu
pkg load video
d=size(A);
dd=d(1);
for i=1:dd Y(i)=A(i,2)+B(i,2)+C(i,2); end
Y=Y/3;
for i=1:dd X(i)=A(i,1); end
X=X/1000;
for i=1:dd X2(i)=X(i)^2; end
c=10;
X2=c*X2;
plot(X,Y,"r",X,X2,"b");
grid on
xlabel("n/1000");
ylabel("f(n)");
uzyskano wykres przedstawiony na rys.11.
W ramach ćwiczeń należy skopiować i uruchomić poniższe programy zapisane w C ++ realizujące algorytmy
o różnych złożonościach obliczeniowych, [9]. Następnie należy je zmodyfikować w analogiczny sposób jak w
przykładzie 9.1 aby uzyskać wykresy ilustrujące te złożoności.

9.2 Przykład 1
Uruchomić a następnie zmodyfikować poniższy algorytm o stałej złożoności obliczeniowej O(1), ilustrując ją
na wykresie, analogicznie jak w przykładzie 9.1.
// C++ program for the constant complexity
#include <stdio.h>
#include <iostream>
#include <cstdlib> //dla funkcji atof
using namespace std;

// Function to check if a
// number is even or odd
void checkEvenOdd(int N)
{
// Find remainder
int r = N % 2;

// Condition for even


if (r == 0) {
cout << "Even" << endl;
}

// Otherwise
else {
cout << "Odd" << endl;
}
}

// Driver Code
int main()
{

41
// Given number N
int N = 101;

// Function Call
checkEvenOdd(N);

return 0;
}

9.3 Przykład 2
Uruchomić a następnie zmodyfikować poniższy algorytm o logarytmicznej złożoności obliczeniowej O(logN ),
ilustrując ją na wykresie, analogicznie jak w przykładzie 9.1.
// C++ program to implement recursive Binary Search
#include <bits/stdc++.h>
using namespace std;

// A recursive binary search function. It returns


// location of x in given array arr[l..r] is present,
// otherwise -1
int binarySearch(int arr[], int l, int r, int x)
{
if (r >= l) {
int mid = l + (r - l) / 2;

// If the element is present at the middle


// itself
if (arr[mid] == x)
return mid;
// If element is smaller than mid, then
// it can only be present in left subarray
if (arr[mid] > x)
return binarySearch(arr, l, mid - 1, x);

// Else the element can only be present


// in right subarray
return binarySearch(arr, mid + 1, r, x);
}

// We reach here when element is not


// present in array
return -1;
}

int main(void)
{
int arr[] = { 2, 3, 4, 10, 40 };
int x = 10;
int n = sizeof(arr) / sizeof(arr[0]);
int result = binarySearch(arr, 0, n - 1, x);
(result == -1)
? cout << "Element is not present in array" << endl
: cout << "Element is present at index " << result << endl;
return 0;

42
}

9.4 Przykład 3
Uruchomić a następnie zmodyfikować poniższy algorytm o liniowej złożoności obliczeniowej O(N ), ilustrując
ją na wykresie, analogicznie jak w przykładzie 9.1.
// C++ code to linearly search x in arr[]. If x
// is present then return its location, otherwise
// return -1

#include <iostream>
using namespace std;

int search(int arr[], int N, int x)


{
int i;
for (i = 0; i < N; i++)
if (arr[i] == x)
return i;
return -1;}

// Driver's code
int main(void)
{
int arr[] = { 2, 3, 4, 10, 40 };
int x = 10;
int N = sizeof(arr) / sizeof(arr[0]);

// Function call
int result = search(arr, N, x);
(result == -1)
? cout << "Element is not present in array" << endl
: cout << "Element is present at index " << result << endl;
return 0;
}

9.5 Przykład 4
Uruchomić a następnie zmodyfikować poniższy algorytm o kwadratowej złożoności obliczeniowej O(N 2 ),
ilustrując ją na wykresie, analogicznie jak w przykładzie 9.1.
// C++ program for the above approach
#include <bits/stdc++.h>

using namespace std;

// Function to find and print pair


bool chkPair(int A[], int size, int x)
{
for (int i = 0; i < (size - 1); i++) {
for (int j = (i + 1); j < size; j++) {
if (A[i] + A[j] == x) {
return 1;
}

43
}
}

return 0;
}

// Driver code
int main()
{
int A[] = { 0, -1, 2, -3, 1 };
int x = -2;
int size = sizeof(A) / sizeof(A[0]);

if (chkPair(A, size, x)) {


cout << "Yes" << endl;
}
else {
cout << "No" << x << endl;
}

return 0;
}

// This code is contributed by Samim Hossain Mondal.

9.6 Przykład 5
Uruchomić a następnie zmodyfikować poniższy algorytm o faktoralnej (N silnia) złożoności obliczeniowej
O(N !), ilustrując ją na wykresie, analogicznie jak w przykładzie 9.1.
// C++ program to print all
// permutations with duplicates allowed
#include <bits/stdc++.h>
using namespace std;

// Function to print permutations of string


// This function takes three parameters:
// 1. String
// 2. Starting index of the string
// 3. Ending index of the string.
void permute(string& a, int l, int r)
{
// Base case
if (l == r)
cout << a << endl;
else {
// Permutations made
for (int i = l; i <= r; i++) {

// Swapping done
swap(a[l], a[i]);

// Recursion called
permute(a, l + 1, r);

44
// backtrack
swap(a[l], a[i]);
}
}
}

// Driver Code
int main()
{
string str = "ABC";
int n = str.size();

// Function call
permute(str, 0, n - 1);
return 0;
}

// This is code is contributed by rathbhupendra

9.7 Przykład 6
Uruchomić a następnie zmodyfikować poniższy algorytm o eksponencjalnej złożoności obliczeniowej O(2N ),
ilustrując ją na wykresie, analogicznie jak w przykładzie 9.1.
// C++ program for the above approach
#include <bits/stdc++.h>

using namespace std;

// Function to find and print pair


bool chkPair(int A[], int size, int x)
{
for (int i = 0; i < (size - 1); i++) {
for (int j = (i + 1); j < size; j++) {
if (A[i] + A[j] == x) {
return 1;
}
}
}

return 0;
}

// Driver code
int main()
{
int A[] = { 0, -1, 2, -3, 1 };
int x = -2;
int size = sizeof(A) / sizeof(A[0]);

if (chkPair(A, size, x)) {


cout << "Yes" << endl;
}
else {
cout << "No" << x << endl;

45
}

return 0;
}

// This code is contributed by Samim Hossain Mondal.


Wniosek: Algorytmy, w zależności złożoności obliczeniowej, mogą być oceniane następująco:
1. O(1) – doskonałe,

2. O(logN ) – dobre,
3. O(N ) – słabe,
4. O(N logN ) – złe,
5. O(N !), O(cN ), O(N c ) – bardzo złe.

Należy jednak wziąć pod uwagę, że możemy napotkać na problemy o takim stopniu trudności, że istnienie nawet
bardzo złego algorytmu dla jego rozwiązania będzie musiało nas zadowolić.

Literatura
[1] https://devdocs.io/cpp
[2] Grębosz J., Symfonia C++ standard, tomy 1, 2, „Edition 2000”, wydanie 3, Kraków 2015.
[3] http://www.ifj.edu.pl/∼grebosz
[4] Rychlicki W., Od matematyki do programowania, Helion, 2011

[5] www.MiroslawZelent.pl, Kurs C ++


[6] Cormen T.H., Algorytmy bez tajemnic, „Helion”, Gliwice 2013.
[7] Allain A., C++. Przewodnik dla początkujących, „Helion”, Gliwice 2014.

[8] Stanley B. Lippman, Podstawy języka C ++ , WNT, Warszawa 1994


[9] Complete Guide On Complexity Analysis – Data Structure and Algorithms Tutorial,
www.geeksforgeeks.org › complete-guide-on-complexity-analysis

46

You might also like