6. Pętle while Przykłady 6.1. Napisz program, który, bez użycia rekurencji, wypisze na ekran liczby naturalne od pewnego danego n do 0 włącznie, w kolejności malejącej, po jednej liczbie na linię. Uwaga! Kod rozwiązania znajduje się niżej. Funkcje rekurencyjne w językach takich jak C++, Java czy Python, zajmują bardzo dużo pamięci. Dodatkowo ich zapotrzebowanie na pamięć często rośnie wraz ze wzrostem wartości przekazywanych argumentów. Na przykład funkcja odliczod zdefiniowana w poprzednim rozdziale, gdy wywołana z argumentem 1000, wywoła samą siebie z argumentem 999, następnie 998, 99 itd. aż do 0. Oznacza to 1001 wywołań tej samej funkcji, a każde wywołanie zajmuje pewną ilość miejsca w pamięci (zależnie od liczby i typów argumentów). Uwaga! Używanie rekurencji wymaga pewnego stopnia ostrożności. Wywołanie funkcji rekurencyjnej z wieloma argumentami może prowadzić do tzw. przepełnienia stosu. Nie oznacza to jednak, że rekurencja nie jest przydatna. Istnieją programy, które o wiele łatwiej napisać jest rekurencyjnie niż iteracyjnie (tj. przy użyciu pętli). Istnieją wręcz obliczenia, których bez rekurencji wykonać się nie da (np. funkcja Ackermanna). Dlatego zamiast mnożyć wywołania, z których każde posiada swój zestaw argumentów, możemy użyć zmiennej, której wartość będziemy wielokrotnie zmieniać i za każdym razem wykorzystywać jej wartość do naszych obliczeń. Do takiego wielokrotnego wykonywania tego samego fragmentu kodu służą pętle. W programie powyżej wykorzystana jest pętla while, która powtarza wykonywanie danego jej fragmentu kodu tak długo, jak długo spełniony jest pewien dany warunek. Pętla while wyglądem przypomina wyrażenie if. Najpierw umieszczamy słowo kluczowe while, następnie w nawiasach okrągłych warunek, a na końcu w nawiasach klamrowych fragment kodu do powtarzania. 41
Uwaga! Pętla while przypomina wyrażenie if nie tylko wyglądem, ale i zachowaniem. Wyrażenie if sprawdza warunek, po czym wykonuje fragment kodu, jeżeli warunek był spełniony lub omija go w przeciwnym wypadku. Podobnie pętla while sprawdza warunek, po czym albo wykonuje kod (warunek jest spełniony) albo go pomija. Różnica polega na tym, że po wykonaniu przypisanego fragmentu kodu, pętla while ponownie sprawdza warunek i decyduje, czy wykonać kod itd. aż warunek nie będzie spełniony. Spójrzmy teraz na rozwiązanie przykładu: 5 int n; 6 cin >> n; 8 while (n >= 0) { 9 cout << n << endl; 10 n = n-1; 11 } 12 1 return 0; 14 } W liniach 5-6 pobieramy od użytkownika wartość liczby n i przechowujemy ją w zmiennej n. Mimo, że kod ten znajduje się poza pętlą, to dokonuje on inicjalizacji pętli, czyli ustawia wartości zmiennych, którymi pętla będzie się posiłkować. Następnie w linii 8 znajduje się warunek. Pętla będzie wykonywana tak długo, jak warunek jest spełniony. Chcemy wypisywać kolejne liczby aż zejdziemy poniżej 0, stąd warunek n >= 0. Następnie w linii 9 wypisujemy obecną wartość n, a w linii 10 zmniejszamy tę wartość o 1. W ten sposób następna iteracja (powtórzenie pętli) wykonywana będzie dla liczby o jeden mniejszej. Nazywamy to krokiem pętli. Kod przy pętli wykonywany będzie po kolei dla coraz mniejszych wartości n, aż dojdzie do 1, dla którego warunek nie będzie spełniony i wykonywanie pętli zakończy się. 42
6.2. Napisz program, który wypisze na ekran kolejne liczby naturalne od 0 do pewnego danego n, bez użycia rekurencji. 5 int n; 6 cin >> n; 8 int i = 0; 9 while (i <= n) { 10 cout << i << endl; 11 i = i+1; 12 } 1 14 return 0; 15 } Jeżeli chcemy wypisać liczby w przeciwnym kierunku, tj. od 0 do n, nie wystarczy nam już tylko jedna zmienna n. Musimy dołożyć do niej kolejną zmienną i, która zapamiętuje jaka następna liczba powinna zostać wypisana. Zmienna ta pełni rolę tzw. iteratora, czyli zmiennej opisującej obecne położenie w pewnym zestawie liczb (tutaj kolejne liczby naturalne od 0 do n). Pozwala to zrobić bez wprowadzania zmian do tego zestawu (tutaj n pozostaje bez zmian, w odróżnieniu od poprzedniego zadania). 6.. Napisz program, który obliczy sumę liczb od 1 do n włącznie, bez użycia rekurencji ani wzoru Gaussa (tj. n(n+1) 2 ). 5 int n; 6 cin >> n; 8 int i = 1; 9 int suma = 0; 10 while (i <= n) { 11 suma = suma + i; 12 i = i+1; 1 } 4
14 15 cout << suma << endl; 16 return 0; 1 } Program ten jest bardzo podobny do programu z poprzedniego przykładu. Dodana została tu zmienna suma, która przechowuje dotychczasową sumę liczb. Zmienną, która zapamiętuje dotychczasowy wynik działań nazywamy akumulatorem. Początkowa wartość tej zmiennej (linia 9) wynosi 0, jako że nie zsumowaliśmy jeszcze żadnych liczb, a suma niczego wynosi 0. Następnie w pętli (linie 10-1) kolejno przechodzimy po wszystkich liczbach od 1 do n włącznie. Robimy to tak, jak w poprzednim zadaniu, z tą różnicą, że tym razem zamiast wypisywać każdą kolejną liczbę, to dodajemy jej wartość do zmiennej suma (linia 11). Stąd po zakończeniu działania pętli, zmienna suma zawierać będzie sumę wszystkich liczb od 1 do n włącznie. Jej początkowa wartość, zero, powiększona została kolejno o wartość każdej kolejnej liczby naturalnej. To daje nam końcową wartość 0 + 1 + 2 +... + (n 1) + n, czyli wymaganą sumę. Zostaje nam tylko wypisać wynik (linia 15). 6.4. Napisz program, który będzie pobierał od użytkownika liczby aż do napotkania zera, a następnie wypisze na ekran ile liczb pobrał (wliczając końcowe zero). 5 int ile = 0; 6 int n = 1; 8 while (n!= 0) { 9 cin >> n; 10 ile = ile + 1; 11 } 12 1 cout << ile << endl; 14 15 return 0; 16 } W tym programie wewnątrz pętli pobieramy kolejne wartości do zmiennej n i zwiększamy wartość zmiennej ile. 44
Ta druga wartość jest naszym akumulatorem, jako że zapamiętuje ile liczb pobraliśmy do tej pory. Początkowo jest to 0 (linia 5). Następnie z każdym pobraniem kolejnej liczby wartość ta zwiększa się o 1 (linia 10), jako że pobraliśmy o jedną liczbę więcej niż dotychczas (linia 9). Początkowa wartość n wynosi 1. Dokładna wartość nie ma znaczenia, jako że i tak zostanie nadpisana. Ważne jest jedynie to, żeby była różna od 0, aby warunek pętli przy pierwszej iteracji był spełniony. Wewnątrz pętli pobieramy kolejne wartości i zwiększamy wartość akumulatora. Uwaga! Warunek pętli NIE jest magicznym strażnikiem, który przerwie działanie pętli w momencie, w którym warunek nie będzie dłużej spełniony. Warunek pętli jest sprawdzany tylko i wyłącznie na początku iteracji! Stąd po pobraniu 0 w linii 9, linia 10 wciąż zostanie wykonana. Dopiero wtedy zakończy się obecna iteracja, a na początku kolejnej sprawdzony zostanie warunek. 6.5. Napisz program, który wypisze k pierwszych potęg 2, zaczynając od 2 0, gdzie k jest dane przez użytkownika. Uwaga! Są dwa sposoby na wykonanie tego zadania. Pierwszy z nich, z użyciem cmath (pętlą przechodząc po wykładnikach i obliczając potęgi przy użyciu pow) spróbuj napisać samodzielnie. Drugi sposób pokazany jest poniżej. 5 int k; 6 cin >> k; 8 int potega = 1; 9 while (k > 0) { 10 cout << potega << endl; 11 potega = potega*2; 12 k = k-1; 1 } 14 15 return 0; 16 } 45
Akumulator nie musi być za każdym razem obliczany przez sumowanie, możemy używać również mnożenia. W przypadku tego programu, akumulatorem jest zmienna potega, która zapamiętuje wartość kolejnej potęgi do wypisania. Za każdą iteracją zwiększana jest dwukrotnie, co daje nam wartość kolejnej potęgi. Z kolei zmienna k opisuje ile jeszcze potęg pozostało nam do wypisania. Jeżeli osiągnie ona 0, oznacza to, że nie zostało nam już nic i możemy zakończyć działanie pętli, a następnie programu. Zwróć też uwagę, że zmienna ta jest za każdą iteracją zmniejszana, a nie zwiększana, jako że liczba potęg pozostałych do wypisania zmniejsza się za każdym razem. 6.6. Napisz program, który obliczy sumę n danych liczb. Program powinien najpierw pobrać liczbę n, a następnie n liczb, po czym wypisać ich sumę. 5 int n; 6 cin >> n; 8 int i = 0; 9 int suma = 0; 10 while (i < n) { 11 int k; 12 cin >> k; 1 14 suma = suma + k; 15 i = i + 1; 16 } 1 18 cout << suma; 19 return 0; 20 } Często zdarzać się będzie, że będziemy potrzebowali pobrać od użytkownika pewien ciąg liczb o określonej przez użytkownika długości. Co prawda w następnym rozdziale poznamy krótszy sposób zapisania programu powyżej, jednak warto już teraz zrozumieć i zapamiętać ogólny schemat pobierania n liczb. Najpierw musimy oczywiście wiedzieć, ile liczb musimy pobrać. Stąd zaczynamy od stworzenia zmiennej n i pobrania jej wartości od użytkownika (linie 5-6). Następnie tworzymy zmienną i pełniącą funkcję iteratora, która zapamiętuje ile liczb zostało już pobranych (linia 8). Początkowo jest to oczywiście 0. W tym przypadku tworzymy 46
również akumulator suma do przechowywania sumy pobranych już liczb, ale nie jest on częścią ogólnego wzorca. Pętla będzie działać tak długo, jak liczba pobranych liczb (i) jest mniejsza od liczby wszystkich liczb, jakie mieliśmy pobrać (n), stąd warunkiem pętli (linia 10) jest i < n. Wewnątrz pętli, za każdą iteracją tworzymy zmienną i pobieramy do niej liczbę (linie 11-12). Uwaga! Zmienną do której pobieramy liczbę w pętli możemy stworzyć przed rozpoczęciem pętli, a nie w środku. W ten sposób tworzymy tylko jedną zmienną, a nie n zmiennych, z których każda istnieje tylko w czasie swojej iteracji. Łamie to jednak zasadę zmienne tworzymy tak późno jak tylko możliwe. Teoretycznie stworzenie zmiennej przed pętlą prowadzi do zmniejszonego zużycia pamięci. W praktyce nowoczesne kompilatory i tak potrafią wykryć przypadek taki jak w kodzie wyżej i użyć dla każdej z tych n zmiennych tego samego miejsca w pamięci. W końcu musimy wykonać jakieś działanie na pobranej liczbie, tutaj dodać ją do dotychczasowej sumy (linia 14), a następnie zwiększyć wartość iteratora (linia 15). Po zakończeniu działania pętli, zmienna suma zawierać będzie wartość sumy n pobranych od użytkownika liczb, dlatego wypisujemy ją (linia 18) i kończymy działanie programu. Pytania 6.1. Porównaj kod z przykładu 6.1 z funkcją rekurencyjną odliczod z poprzedniego rozdziału, wykonującą to samo zadanie. Zwróć uwagę na podobieństwa i różnice między nimi. 6.2. Skąd bierze się różnica w warunkach znajdujących się w obu tych programach (n < 0 w funkcji rekurencyjnej, n >= 0 w pętli)? 6.. Dlaczego zmienna n w przykładzie 6.4 ma przypisaną początkową wartość, jeżeli jest ona niemal natychmiast nadpisywana w pętli? Czy możemy tę wartość dowolnie zmienić? 6.4. W poprzednim rozdziale zwrócona była uwaga, aby unikać == i!= w warunkach pętli, a w zamian używać nierówności tam, gdzie tylko jest to możliwe. Dlaczego więc w przykładzie 6.4 warunek to n!= 0? Czy możemy go zamienić na jakąś nierówność? Jeżeli tak, jaką? Jeżeli nie, dlaczego? 4
6.5. Który z dwóch poniższych fragmentów kodu skompiluje się? Jeżeli któryś z nich spowoduje błąd kompilacji jaki i dlaczego? Jeżeli oba się kompilują, jaka jest różnica w ich działaniu? 1 // Fragment 1 2 int n = 5; int i = 0; 4 int k; 5 while (i < n) { 6 cin >> k; i = i + 1; 8 } 9 cout << k; 10 11 // Fragment 2 12 int n = 5; 1 int i = 0; 14 while (i < n) { 15 int k; 16 cin >> k; 1 i = i + 1; 18 } 19 cout << k; Zadania Uwaga! W rozwiązaniach zadań poniżej, o ile nie zaznaczono inaczej, nie używaj cmath ani rekurencji. 6.1. Napisz program, który wypisze na ekran liczby nieparzyste od danego n do 1 włącznie, w kolejności malejącej. Zwróć uwagę, że n może być parzyste musisz to sprawdzić przed wykonaniem pętli i odpowiednio zmienić wartość n, aby było nieparzyste. 6.2. Napisz program, który wypisze k pierwszych wielokrotności 10, gdzie k jest dane przez użytkownika. Za pierwszą wielokrotność 10 uznajemy 10. 6.. Napisz program, który wypisze reszty z dzielenia liczby n danej przez użytkownika, przez wszystkie kolejne liczby naturalne od 1 do n włącznie. 48
6.4. Napisz funkcję, który znajdzie liczbę dzielników danej liczby całkowitej (jeżeli jest to dla ciebie zbyt trudne, rozwiąż najpierw poprzednie zadanie). Następnie napisz program, który przy użyciu tej funkcji będzie w nieskończoność wypisywał kolejne liczby pierwsze, zaczynając od 2. 6.5. Napisz program, który obliczy wartość silni z danej przez użytkownika liczby n. Dla przypomnienia: silnia to iloczyn kolejnych liczb naturalnych od 1 do n włącznie. 6.6. Zmodyfikuj program z przykładu 6.5 tak, aby obliczał i wypisywał wyłącznie wartość k-tej potęgi 2. Wymaga to wyłącznie zmiany położenia jednej linii kodu. 6.. Napisz program, który obliczy n k, gdzie n i k są dane przez użytkownika. 6.8. Napisz program, który policzy sumę n pierwszych liczb trójkątnych. Liczba trójkątna to liczba kul, które potrzebne są do zbudowania trójkąta równobocznego o danej podstawie. Kolejne liczby trójkątne to 1,, 6, 10, 15, 21, 28 itd. 6.9. Napisz program, który pobierze od użytkownika n liczb (gdzie n jest dane), a następnie wypisze na ekran ich średnią arytmetyczną. 6.10. Napisz program, który pobierze od użytkownika n liczb, a natępnie wypisze na ekran najmniejszą z nich. Podpowiedź: potrzebujesz zmiennej, która zapamiętywać będzie wartość najmniejszej z dotychczas pobranych liczb. Jej wartość powinna być zmieniana tylko wtedy, gdy nowo pobrana liczba jest mniejsza od dotychczasowego minimum. Rozszerzenie 6.1. Napisz program, który będzie znajdował i wypisywał na ekran rozkład danej liczby na czynniki pierwsze. Postaraj się, aby twój program działał jak najszybciej. Na przykład rozkład na czynniki pierwsze liczby 999 999 9 (która jest pierwsza) powinien pojawiać się na ekranie niemal natychmiast (kilka sekund to za długo). 6.2. Regresja liniowa. Napisz program, który dla danego zestawu punktów na płaszczyznie, znajdzie linię prostą, której sumaryczna odległość od każdego z punktów jest jak najmniejsza. 6.. Klastrowanie algorytmem k-średnich. Napisz program, który dany zestaw punktów na płaszczyznie podzieli na dwa zestawy takie, że sumaryczna odległość wszystkich punktów od środka ich zestawu jest jak najmniejsza. 49