8. Wektory Przykłady 8.1. Napisz program, który pobierze od użytkownika 10 liczb, a następnie wypisze je w kolejności odwrotnej niż podana. Uwaga! Kod poniżej. To zadanie można rozwiązać przy użyciu wiedzy przedstawionej dotychczas w tym podręczniku. Można stworzyć 10 zmiennych, od a1 do a10, potem do każdej z nich kolejno pobrać wartość, a w końcu każdą z nich po kolei wypisać, zaczynając od a10, a kończąc na a1. Taki program będzie działać. Będzie prosty do napisania. Co jednak gdyby liczb miało być nie 10, a 100 czy 1000? Sprytniejsza osoba może sobie napisać program, który wygeneruje taki program dla dowolnej, z góry określonej liczby. Co jednak, jeżeli ta liczba nie jest z góry określona? Uwaga! O rozwiązaniu takim jak to powyżej mówimy, że się nie skaluje. Oznacza to, że rozwiązanie takie działa bardzo dobrze dla małych liczb, jednak bardzo szybko staje się niewystarczające jeżeli te liczby zaczniemy zwiększać. Rozwiązanie, które się nie skaluje niekoniecznie jest złe. Jeżeli wiemy, że pewne liczby zawsze pozostaną małe, użycie szybkiego rozwiązania, które się nie skaluje może być dobrym pomysłem. Na przykład w zadaniu powyżej, gdyby liczb miało być 3, stworzenie 3 osobnych zmiennych byłoby świetnym rozwiązaniem. Problem polega na tym, że w rzeczywistości niemal nigdy nie możemy być pewni, że liczby zawsze pozostaną małe. Możemy na przykład użyć wektorów. Wektor pozwala na przechowanie w jednej zmiennej zestawu wartości o określonym typie. Możemy na przykład napisać vector<int> ns(10);, co stworzy wektor liczb całkowitych o nazwie ns i 10 wartościach: od ns[0] do ns[9]. Każdą z tych wartości możemy wykorzystywać jako osobną zmienną typu int. asdflkj 55
Uwaga! W informatyce liczymy zawsze od 0. Stąd pierwszą wartością w wektorze vector<int> ns(10) jest ns[0], a ostatnią ns[9], zaś wartość ns[10] nie istnieje. W ten sposób możemy szybko stworzyć dużą liczbę zmiennych, jednak nadal mamy problem z pobraniem i wypisaniem ich wszystkich. Tutaj też jednak jest szybkie rozwiązanie. W nawiasie kwadratowym (np. ns[3]) nie musimy umieszczać konkretnej liczby. Może być to wyrażenie matematyczne, zmienne, a nawet wywołania funkcji. Cokolwiek, co ostatecznie da nam liczbę całkowitą się nada. Dzięki temu te 10 liczb możemy pobrać przy użyciu pętli: 1 for (int i = 0; i < 10; i++) { 2 cin >> ns[i]; 3 } a następnie wypisać je przy użyciu kolejnej pętli, idącej w drugą stronę: 1 for (int i = 9; i >= 0; i = i-1) { 2 cout << ns[i] << endl; 3 } Należy także pamiętać, że aby wykorzystywać w programie wektory, musimy na początku programu dopisać linię #include <vector>. W całości rozwiązanie tego zadania wygląda tak: 1 #include <vector> 2 #include <iostream> 3 using namespace std; 4 5 int main() { 6 vector<int> ns(10); 7 for (int i = 0; i < 10; i = i+1) { 8 cin >> ns[i]; 9 } 10 11 for (int i = 9; i >= 0; i = i-1) { 12 cout << ns[i] << endl; 13 } 14 15 return 0; 16 } 56
8.2. Napisz program, który pobierze od użytkownika n liczb, a następnie wypisze je w kolejności odwrotnej niż podana. 1 #include <vector> 2 #include <iostream> 3 using namespace std; 4 5 int main() { 6 int n; 7 cin >> n; 8 9 vector<int> ns(n); 10 for (int i = 0; i < n; i = i+1) { 11 cin >> ns[i]; 12 } 13 14 for (int i = n-1; i >= 0; i = i-1) { 15 cout << ns[i] << endl; 16 } 17 18 return 0; 19 } Dzięki wektorom możemy również stworzyć zestaw wartości o wielkości nieznanej w momencie pisania kodu. Nie tylko indeks (wartość w nawiasie kwadratowym) może być wyrażeniem matematycznym czy zmienną, ale także rozmiar wektora. Dzięki temu możemy pobrać od użytkownika liczbę n (linie 6-7), a następnie stworzyć wektor o n wartościach (linia 9). W końcu pobieramy te wartości w pętli (linie 10-12) i wypisujemy je na ekran w kolejności odwrotnej (linie 14-16). Program ten nie różni się bardzo od rozwiązania poprzedniego przykładu, jedynie stała wartość 10 została zamieniona na zmienną n. Warto zwrócić uwagę na to, jak ta zamiana się dokonała, szczególnie w inicjalizacji i warunkach pętli. W pierwszej pętli początkowa wartość nie zmienia się (jest to wciąż 0). Jednak warunek zależał od liczby 10 w poprzednim zadaniu, stąd tutaj zależy od n. Nasz warunek to i < n. Nie chcemy, aby zmienna i osiągnęła wartość n, ponieważ taki indeks nie istnieje w wektorze ns. Moglibyśmy co prawda napisać i <= n-1, jednak zapis i < n jest krótszy i zdecydowanie częściej stosowany w takiej sytuacji. Podobnie w drugiej z pętli, zaczynamy od wartości n-1, ponieważ jest to ostatni indeks znajdujący się w wektorze ns. Chcemy natomiast dojść do wartości 0, stąd warunek to i >= 0, a nie po prostu i > 0. 57
8.3. Napisz program, który pobierze od użytkownika n liczb, a następnie dodatkową liczbę a i sprawdzi, ile razy a znajduje się w danym zestawie liczb. 1 #include <vector> 2 #include <iostream> 3 using namespace std; 4 5 int main() { 6 // pobierz n liczb do wektora 7 int n; 8 cin >> n; 9 10 vector<int> ns(n); 11 for (int i = 0; i < n; i = i+1) { 12 cin >> ns[i]; 13 } 14 15 // pobierz dodatkowa liczbe i sprawdz 16 // ile razy znajduje sie w wektorze 17 int a; 18 cin >> a; 19 int count = 0; 20 for (int i = 0; i < n; i = i+1) { 21 if (ns[i] == a) { 22 count = count+1; 23 } 24 } 25 26 // wypisz wynik na ekran 27 cout << count << endl; 28 29 return 0; 30 } Linie 7-13 tego programu odpowiadają za pobranie n liczb od użytkownika i przechowanie ich w wektorze. Następnie w liniach 17-18 pobieramy dodatkową liczbę, której będziemy wyszukiwać pośród pobranych wcześniej wartości. Dopiero potem zaczyna się właściwe poszukiwanie. Tworzymy zmienną count (linia 19), która pełnić będzie funkcję akumulatora zapamiętywać będzie ile razy dotychczas napotkaliśmy wartość a. Początkowo jest to 0, jako że jeszcze nie zaczęliśmy szukać. Następnie pętlą przechodzimy po wszystkich indeksach wektora ns, od 0 włącznie do n wyłącznie (linia 20). Wartość pod każdym indeksem przyrównujemy 58
do a (linia 21) i jeżeli taka równość zachodzi, zwiększamy count o 1 (linia 22). Po zakończeniu działania pętli zmienna count zawiera nasz wynik, stąd wypisujemy ją na ekran (linia 27). 8.4. Napisz funkcję contains, która przyjmować będzie jako argumenty wektor i liczbę, a następnie sprawdzi, czy dana liczba znajduje się w danym wektorze. Napisz przykładowy program, który będzie wykorzystywał tę funkcję. 1 #include <vector> 2 #include <iostream> 3 using namespace std; 4 5 bool contains(vector<int> ns, int n) { 6 for (int i = 0; i < ns.size(); i = i+1) { 7 if (ns[i] == n) { 8 return true; 9 } 10 } 11 12 return false; 13 } 14 15 int main() { 16 int n; 17 cin >> n; 18 19 vector<int> ns(n); 20 for (int i = 0; i < n; i = i+1) { 21 cin >> ns[i]; 22 } 23 24 int a; 25 cin >> a; 26 if (contains(ns, a)) { 27 cout << "tak" << endl; 28 } else { 29 cout << "nie" << endl; 30 } 31 32 return 0; 33 34 } 59
Wektory możemy przekazywać jako argumenty funkcji, jak każdą inną zmienną. Przy definicji funkcji możemy umieścić wektor jako argument (linia 5), a następnie przekazać wartość do funkcji podając jej nazwę (linia 26). Należy jednak zwrócić uwagę na kwestię rozmiaru wektora. Jak zapewne pamiętasz, przy tworzeniu funkcji deklarujemy jej argumenty podając dla każdego z nich typ i nazwę. Rozmiar wektora nie jest częścią ani jego typu ani nazwy, stąd nie możemy narzucić, że wektor przekazany do funkcji będzie mieć dokładną, określoną przez nas, liczbę elementów. Musimy jednak w jakiś sposób poznać rozmiar wektora, który otrzymujemy wewnątrz funkcji. Służy do tego metoda size. Podajemy najpierw nazwę wektora, którego rozmiar chcemy poznać, następnie kropkę i słowo size, za którym umieszczamy parę okrągłych nawiasów (linia 6). Uwaga! Metoda to pewien specjalny rodzaj funkcji. Każde jej wywołanie musi być przywiązane do zmiennej (podanej przed kropką) określonego typu. Zmienna ta jest przekazywana jako argument do metody, jednak w nieco inny sposób niż inne argumenty. Jak to dokładnie działa dowiesz się w następnych dwóch rozdziałach. Oczywiście metodę size możemy wykorzystywać dla każdego wektora i w każdym miejscu, nie tylko dla wektorów liczb całkowitych wewnątrz funkcji. Sama funkcja contains jest dość prosta. Zgodnie z treścią zadania przyjmuje dwa argumenty: wektor liczb ns oraz poszukiwaną liczbę n. Zwraca typ bool: prawdę, jeżeli ns zawiera wartość n lub fałsz w przeciwnym wypadku. Wewnątrz funkcji znajduje się pętla, która przechodzi kolejno po wszystkich indeksach, od 0 do ns.size()-1 włącznie. Dla każdego z nich sprawdza, czy znajdująca się pod nim wartość jest równa n. Jeżeli tak, zwraca prawdę, co kończy działanie funkcji. Sprawdzanie kolejnych indeksów nie ma sensu, jako że już znamy odpowiedź. Jeżeli pętla zakończy swoje działanie i dotrzemy do linii 12, oznacza to, że wartość n nie znajduje się w wektorze. Zwracamy wtedy fałsz kończąc działanie funkcji. Funkcję tę można napisać wykorzystując akumulator typu bool, który przechowuje informację o tym, czy widzieliśmy już wartość n w wektorze. Początkowo ma wartość false, jako że nie mogliśmy widzieć n jeżeli nie zaczęliśmy szukać. Następnie pętlą podobną jak w programie wyżej sprawdzamy każdy element wektora i ustawiamy akumulator na true jeżeli znajdziemy n. Na końcu zwracamy wartość akumulatora. Rozwiązanie podane na początku tego przykładu ma jednak tę zaletę, że w wielu przypadkach nie musimy sprawdzać wszystkich wartości w wektorze. 60
8.5. Napisz funkcję, która przyjmować będzie jako argument wektor zawierający liczby całkowite, a zwróci wektor zawierający tylko liczby parzyste, w tej samej liczbie i kolejności, w jakiej występowały w danym wektorze. Napisz przykładowy program, który będzie wykorzystywać tę funkcję. 1 #include <iostream> 2 #include <vector> 3 using namespace std; 4 5 vector<int> even_filter(vector<int> ns) { 6 vector<int> result; 7 for (int i = 0; i < ns.size(); i = i+1) { 8 if (ns[i] % 2 == 0) { 9 result.push_back(ns[i]); 10 } 11 } 12 13 return result; 14 } 15 16 // przykladowy program 17 int main() { 18 int n; 19 cin >> n; 20 21 vector<int> ns(n); 22 for (int i = 0; i < n; i = i+1) { 23 cin >> ns[i]; 24 } 25 26 vector<int> evens = even_filter(ns); 27 for (int i = 0; i < evens.size(); i = i+1) { 28 cout << evens[i] << endl; 29 } 30 31 return 0; 32 } Wektor możemy przekazywać jako argument funkcji, ale możemy go także zwracać jako wynik. Tutaj funkcja even_filter przyjmuje jako argument wektor zawierający pewne liczby, a jako wynik zwraca inny wektor, zawierający wyłącznie liczby parzyste. Jedyny problem polega na tym, że nie wiemy jaki będzie rozmiar wynikowego 61
wektora. Moglibyśmy najpierw jedną pętlą policzyć ile liczb parzystych znajduje się w danym zestawie, a następnie stworzyć wektor o pożądanym rozmiarze. Istnieje jednak prostsze rozwiązanie. Wektory można dowolnie rozszerzać. Dzięki temu możemy stworzyć pusty wektor, do którego dodawać będziemy kolejne wartości. W linii 6 pominięty został rozmiar tworzonego wektora, co oznacza, że będzie on pusty. Następnie w pętli wykorzystujemy metodę push_back, która dodaje na końcu wektora wartość przekazaną jako argument (linia 9). Przechodzimy więc pętlą po wszystkich elementach ns i sprawdzamy ich parzystość. W przypadku gdy sprawdzana wartość jest parzysta, przekazujemy ją do metody push_back. Na końcu wektora result tworzony jest wtedy nowy element, który otrzymuje wartość przekazaną do push_back. Wywołanie funkcji zwracającej wektor nie różni się niczym od wywołania innych funkcji. Podajemy najpierw nazwę funkcji, a następnie w nawiasach jej argumenty. Przykład widzimy w linii 26. Tutaj wynik funkcji umieszczany jest w nowej zmiennej, co pozwoli na jego wykorzystanie w późniejszych obliczeniach. Uwaga! Poniższy kod prezentuje sposób wykorzystania funkcji zwracającej wektor, którego lepiej jest unikać, mimo że jest technicznie poprawny i zwróci ten sam wynik co linie 26-29 kodu z przykładu powyżej. 1 for (int i = 0; i < even_filter(ns).size(); i++) { 2 cout << even_filter(ns)[i] << endl; 3 } Problem z tym fragmentem kodu polega na tym, że w każdym obrocie pętli funkcja even_filter wywoływana jest dwa razy, tworząc za każdym razem dwa nowe, identyczne wektory. Nie dosyć, że dla dużych wektorów operacja ta może trwać dłuższy czas, to do tego zająć może dużo pamięci. Dużo lepiej jest wywołać tę funkcję raz i jej wynik przypisać do zmiennej. Pytania 8.1. Spójrz na kod z przykładu 8.2. W jaki sposób zmieni się jego zachowanie, jeżeli warunek pętli w linii 14 zmienimy na i > 0? 8.2. Co się stanie, jeżeli zmienimy warunek w linii 10 na i <= n? 62
Zadania Uwaga! W zadaniach, w których należy napisać funkcję, napisz również przykładowy program, który wykorzystuje tę funkcję i pozwala przetestować poprawność jej działania. 8.1. Napisz program, który pobierze od użytkownika zestaw liczb a następnie pewną liczbę całkowitą k, a następnie wypisze na ekran ile liczb w danym zestawie jest podzielnych przez k. 8.2. Napisz funkcję, która z danego wektora liczb całkowitych wybierze i zwróci najmniejszą. 8.3. Napisz funkcję, która policzy ile razy dana liczba występuje w danym zestawie liczb. Napisz program, który będzie wykorzystywać tę funkcję do policzenia ile razy liczba 0 występuje w danym przez użytkownika zestawie. 8.4. Napisz program, który pobierze od użytkownika pewien zestaw liczb całkowitych, a następnie znajdzie i wypisze najmniejszą liczbę naturalną, która się w nim nie znajduje. 8.5. Napisz funkcję, która dla danej liczby naturalnej znajdzie i zwróci wektor zawierający wszystkie jej dzielniki w kolejności rosnącej. Postaraj się, aby twoja funkcja zwracała wynik natychmiast nawet dla liczb o wartościach bliskich miliarda. 8.6. Napisz funkcję, która przyjmie jako argument liczbę n, a następnie pobierze od użytkownika n liczb i zwróci je w formie wektora. Możesz tej funkcji używać w swoich późniejszych programach dla szybszego pobierania wartości do wektora: 1 int n; 2 cin >> n; 3 vector<int> ns = pobierz(n); 8.7. Napisz funkcję, która przyjmować będzie jako argument wektor zawierający liczby całkowite, a zwróci wektor zawierający tylko liczby dodatnie, w tej samej liczbie i kolejności, w jakiej występowały w danym wektorze. 8.8. Napisz funkcję, która przyjmie jako argumenty wektor oraz indeks i zwróci wektor, który posiada te same elementy, za wyjątkiem elementu pod danym indeksem. Kolejność elementów ma pozostać taka sama. 63
8.9. Napisz funkcję, która przyjmie jako argumenty wektor i dwa indeksy: i oraz j. Funkcja ma zwrócić wektor zawierający elementy znajdujące się na indeksach od i do j włącznie w wektorze danym jako argument. Jeżeli i > j, wynikiem powinien być pusty wektor. Wyjątkiem jest sytuacja, gdy j = 1, w której funkcja powinna zwrócić elementy od indeksu i aż do końca danego wektora. 8.10. Na przyjęciu zorganizowanym przez Bajtazara obecnych będzie n osób. Każda osoba ma jasno określone preferencje na temat tego kto ma siedzieć po jej prawej stronie. Każda osoba posiada tylko jedną taką preferencję, inną od wszystkich pozostałych. Ile stołów będzie potrzebnych, aby dogodzić wszystkim preferencjom gości? Najpierw podana jest liczba n, a następnie n liczb a i (0 i n). Liczba a i określa numer osoby, która ma siedzieć po prawej stronie osoby z numerem i. Jako wynik podaj jedną liczbę: minimalną liczbę wymaganych stołów. Dodatkowo, w kolejnej linii, możesz podać liczby opisujące ile osób powinno siedzieć przy każdym ze stołów. Rozszerzenie 8.1. Napisz funkcję reverse, która odwróci kolejność elementów wektora danego jako argument w miejscu. To znaczy, poniższy fragment kodu powinien wypisać 9 8 7 6 5 4 3 2 1 0. 1 vector<int> ns; 2 for (int i = 0; i < 10; i++) { 3 ns.push_back(i); 4 } 5 6 reverse(ns); // ewentualnie reverse(&ns); 7 8 for (int i = 0; i < ns.size(); i++) { 9 cout << ns[i] << " "; 10 } 8.2. Napisz przeładowania operatorów << i >> dla wektorów i odpowiednich strumieni. Pozwoli ci to na pobieranie i wypisywanie wektorów przy użyciu strumieni cin i cout, ale także strumieni plików. 8.3. W jaskini smoka znajduje się n cennych przedmiotów. Każdy z nich ma swoją własną objętość o i i wartość w i. Plecak pomieści co najwyżej K jednostek objętości. Napisz program, który wybierze skarby o największej sumarycznej wartości tak, aby wszystkie zmieściły się w plecaku. 64