break, continue i pętle zagnieżdżone
Pętla nie zawsze musi dobiec do końca. Czasem trzeba wyjść, gdy znajdziemy to, czego szukamy, albo pominąć jeden obieg i lecieć dalej. A gdy pętla znajdzie się wewnątrz innej pętli — trzeba dokładnie wiedzieć, której z nich dotyczy nasze polecenie.
Czego się dziś nauczysz
- Przerwiesz pętlę w momencie znalezienia szukanej wartości
- Pominiesz jeden obieg pętli bez przerywania całości
- Wyjaśnisz, dlaczego
breakwswitchwewnątrz pętli nie kończy pętli - Napiszesz pętlę w pętli i policzysz, ile razy wykona się blok wewnętrzny
- Wyjdziesz z dwóch pętli naraz, nie używając
goto
Instrukcja break
break natychmiast kończy pętlę. Program przeskakuje za jej klamrę zamykającą i leci dalej — pozostałe obiegi się nie wykonają.
for (int i = 1; i <= 10; i++)
{
if (i == 4)
{
Console.WriteLine("Znalazlem czworke - koncze.");
break;
}
Console.WriteLine($"Sprawdzam {i}");
}
Console.WriteLine("Po petli.");
Sprawdzam 1
Sprawdzam 2
Sprawdzam 3
Znalazlem czworke - koncze.
Po petli.
Pętla miała wykonać dziesięć obiegów, wykonała cztery. Liczby od 5 do 10 nie zostały nawet sprawdzone.
Typowe zastosowanie: przerwij, gdy znalazłeś
To najczęstszy powód użycia break. Gdy szukasz jednej wartości, nie ma sensu przeglądać reszty:
string tekst = "programowanie";
char szukany = 'm';
int pozycja = -1; // -1 oznacza "nie znaleziono"
for (int i = 0; i < tekst.Length; i++)
{
if (tekst[i] == szukany)
{
pozycja = i;
break; // dalej szukac nie ma po co
}
}
if (pozycja >= 0)
{
Console.WriteLine($"Znak '{szukany}' pierwszy raz na pozycji {pozycja}.");
}
else
{
Console.WriteLine($"Znaku '{szukany}' nie ma w tekscie.");
}
Znak 'm' pierwszy raz na pozycji 6.
Dlaczego pozycja zaczyna od −1
Potrzebna jest wartość, która na pewno nie może być prawdziwym wynikiem. Pozycje w napisie numerujemy od zera, więc zero jest zajęte — jest poprawną odpowiedzią „pierwszy znak”. Minus jeden nigdy poprawną pozycją nie będzie, więc bezpiecznie oznacza „nie znaleziono”. Tak samo działa wbudowana metoda tekst.IndexOf(szukany).
Instrukcja continue
continue kończy bieżący obieg, ale nie pętlę. Program pomija resztę bloku i wraca na początek — do zwiększenia licznika i sprawdzenia warunku.
for (int i = 1; i <= 10; i++)
{
if (i % 2 == 0)
{
continue; // parzyste pomijamy
}
Console.WriteLine($"Nieparzysta: {i}");
}
Nieparzysta: 1
Nieparzysta: 3
Nieparzysta: 5
Nieparzysta: 7
Nieparzysta: 9
| Instrukcja | Co przerywa | Dokąd skacze |
|---|---|---|
break | całą pętlę | za klamrę zamykającą pętli |
continue | tylko bieżący obieg | do warunku (w for — najpierw do części zwiększającej licznik) |
continue w pętli while potrafi zawiesić program
W pętli for licznik zwiększa się zawsze, także przy continue — to część nagłówka pętli. W pętli while licznik zwiększasz ręcznie, więc continue postawione przed i++ pominie tę linię i pętla zatrzyma się na zawsze.
int i = 1;
while (i <= 10)
{
if (i % 2 == 0)
{
continue; // i nigdy nie wzrosnie - petla nieskonczona
}
Console.WriteLine(i);
i++;
}
W pętlach while bezpieczniej jest odwrócić warunek i nie używać continue w ogóle:
int i = 1;
while (i <= 10)
{
if (i % 2 != 0) // zamiast "pomijaj parzyste" - "wypisuj nieparzyste"
{
Console.WriteLine(i);
}
i++;
}
Kiedy continue naprawdę pomaga
Wtedy, gdy pozwala uniknąć głębokiego wcięcia. Zamiast owijać sto linii w if (dane_sa_dobre) { ... }, na początku pętli piszemy if (!dane_sa_dobre) { continue; } i reszta bloku zostaje na płaskim poziomie. Ten wzorzec nazywa się „wczesnym wyjściem” i naprawdę poprawia czytelność.
Pułapka: break w switch wewnątrz pętli
To jedno z najczęstszych nieporozumień u osób, które właśnie poznały switch. Instrukcja break odnosi się do najbliższej otaczającej konstrukcji — a jeśli jest nią switch, to pętla nawet nie drgnie.
while (true)
{
Console.Write("Polecenie: ");
string p = (Console.ReadLine() ?? "").Trim().ToLower();
switch (p)
{
case "pomoc":
Console.WriteLine("Dostepne: pomoc, koniec");
break; // wychodzi ze switch
case "koniec":
break; // TEZ wychodzi tylko ze switch!
default:
Console.WriteLine("Nie znam takiego polecenia.");
break;
}
}
Program nigdy się nie zakończy: break w gałęzi „koniec” wychodzi ze switch, po czym pętla while (true) zaczyna kolejny obieg. Rozwiązanie — flaga sprawdzana przez pętlę:
bool dziala = true;
while (dziala)
{
Console.Write("Polecenie: ");
string p = (Console.ReadLine() ?? "").Trim().ToLower();
switch (p)
{
case "pomoc":
Console.WriteLine("Dostepne: pomoc, koniec");
break;
case "koniec":
dziala = false; // to zakonczy petle
Console.WriteLine("Do zobaczenia.");
break;
default:
Console.WriteLine("Nie znam takiego polecenia.");
break;
}
}
Warunek wraca na swoje miejsce
Przy okazji zniknęło while (true). Nagłówek pętli mówi teraz wprost, kiedy program działa — nie trzeba szukać w środku, gdzie jest wyjście. To zwykle lepszy zapis.
Pętle zagnieżdżone
Wewnątrz pętli może stać kolejna pętla. Zasada jest jedna i wystarczy ją raz zrozumieć: na każdy jeden obieg pętli zewnętrznej pętla wewnętrzna przebiega całą swoją drogę od początku do końca.
for (int i = 1; i <= 3; i++)
{
Console.WriteLine($"Zewnetrzna: i = {i}");
for (int j = 1; j <= 2; j++)
{
Console.WriteLine($" wewnetrzna: j = {j}");
}
}
Zewnetrzna: i = 1
wewnetrzna: j = 1
wewnetrzna: j = 2
Zewnetrzna: i = 2
wewnetrzna: j = 1
wewnetrzna: j = 2
Zewnetrzna: i = 3
wewnetrzna: j = 1
wewnetrzna: j = 2
Pętla zewnętrzna wykonała się 3 razy, wewnętrzna — 3 × 2 = 6 razy. To mnożenie jest ważne: dwie pętle po 1000 obiegów to milion wykonań bloku wewnętrznego.
Klasyczne zastosowanie: tabela
Pętla zewnętrzna odpowiada za wiersze, wewnętrzna za kolumny wewnątrz wiersza. Kluczowa jest linia po pętli wewnętrznej — to ona kończy wiersz:
// naglowek
Console.Write(" ");
for (int j = 1; j <= 10; j++)
{
Console.Write($"{j,5}");
}
Console.WriteLine();
Console.WriteLine(new string('-', 54));
// wiersze
for (int i = 1; i <= 10; i++)
{
Console.Write($"{i,3}|"); // etykieta wiersza, bez nowej linii
for (int j = 1; j <= 10; j++)
{
Console.Write($"{i * j,5}"); // Write, nie WriteLine!
}
Console.WriteLine(); // dopiero tu konczymy wiersz
}
1 2 3 4 5 6 7 8 9 10
------------------------------------------------------
1| 1 2 3 4 5 6 7 8 9 10
2| 2 4 6 8 10 12 14 16 18 20
3| 3 6 9 12 15 18 21 24 27 30
4| 4 8 12 16 20 24 28 32 36 40
5| 5 10 15 20 25 30 35 40 45 50
6| 6 12 18 24 30 36 42 48 54 60
7| 7 14 21 28 35 42 49 56 63 70
8| 8 16 24 32 40 48 56 64 72 80
9| 9 18 27 36 45 54 63 72 81 90
10| 10 20 30 40 50 60 70 80 90 100
Write wewnątrz, WriteLine na zewnątrz
Najczęstszy błąd przy tabelach: użycie Console.WriteLine w pętli wewnętrznej. Wtedy każda liczba ląduje w osobnej linii i zamiast tabeli dostajesz stumetrową kolumnę. Nowa linia należy do wiersza, więc jej miejsce jest po pętli wewnętrznej.
Gdy wewnętrzna pętla zależy od zewnętrznej
Warunek pętli wewnętrznej może korzystać ze zmiennej pętli zewnętrznej. Tak rysuje się trójkąty:
int wysokosc = 5;
for (int i = 1; i <= wysokosc; i++)
{
for (int j = 1; j <= i; j++) // uwaga: warunek to j <= i
{
Console.Write('*');
}
Console.WriteLine();
}
*
**
***
****
*****
W pierwszym wierszu pętla wewnętrzna wykona się raz, w drugim dwa razy, i tak dalej. Ten sam efekt można osiągnąć jedną linią — Console.WriteLine(new string('*', i)); — i w prawdziwym programie tak właśnie byłoby lepiej. Wersja z pętlą służy do zrozumienia mechanizmu.
Wyjście z dwóch pętli naraz
break kończy tylko tę pętlę, w której stoi — najbliższą. Gdy szukamy czegoś w tabeli i chcemy przerwać całe przeszukiwanie, jedno break nie wystarczy:
for (int i = 1; i <= 3; i++)
{
for (int j = 1; j <= 3; j++)
{
if (i * j == 4)
{
Console.WriteLine($"Znalazlem: {i} x {j}");
break; // wychodzi tylko z petli po j
}
}
// ...i tu petla po i leci dalej
}
Najczytelniejsze rozwiązanie to flaga — zmienna logiczna sprawdzana także przez pętlę zewnętrzną:
bool znaleziono = false;
for (int i = 1; i <= 3 && !znaleziono; i++)
{
for (int j = 1; j <= 3; j++)
{
if (i * j == 4)
{
Console.WriteLine($"Znalazlem: {i} x {j}");
znaleziono = true;
break; // konczy petle wewnetrzna
}
}
// warunek zewnetrznej sprawdzi flage i tez sie zakonczy
}
if (!znaleziono)
{
Console.WriteLine("Nie znaleziono.");
}
Znalazlem: 2 x 2
Istnieje też goto — i lepiej z niego nie korzystać
C# ma instrukcję goto etykieta, którą dałoby się wyskoczyć z dowolnej liczby pętli. Działa, ale burzy porządek programu tak bardzo, że w praktyce jej nie używamy. Docelowym rozwiązaniem jest wydzielenie przeszukiwania do osobnej metody i zakończenie jej instrukcją return — pokażemy to w bloku o funkcjach.
Kiedy break pomaga, a kiedy szkodzi
| Dobre użycie | Podejrzane użycie |
|---|---|
| Szukasz pierwszego pasującego elementu i po znalezieniu nie masz już nic do roboty. | Kilka instrukcji break w jednej pętli, w różnych miejscach — nie da się już powiedzieć, kiedy pętla się kończy. |
| Sprawdzasz warunek, którego nie da się zapisać w nagłówku pętli (bo zależy od danych z wnętrza obiegu). | break tam, gdzie wystarczyłby warunek w nagłówku while — wtedy nagłówek kłamie o tym, jak długo pętla działa. |
| Przerywasz kosztowne obliczenia, gdy dalszy ciąg nie ma sensu (np. liczba okazała się złożona). | break jako sposób na wyjście z sytuacji, w której program i tak nie wie, co robić — wtedy problem jest gdzie indziej. |
Prosta zasada na start: jeden break w pętli to zwykle dobrze, trzy to znak, że pętlę trzeba przemyśleć od nowa.
Użyte w tej lekcji elementy
| Nazwa | Zwraca | Działanie |
|---|---|---|
tekst[i] | char |
Znak napisu o podanym numerze. Numery zaczynają się od zera, ostatni to tekst.Length - 1. Numer spoza zakresu przerywa program. |
tekst.IndexOf(z) | int |
Gotowa wersja przeszukiwania z tej lekcji: numer pierwszego wystąpienia znaku lub fragmentu, a -1, gdy nie ma. LastIndexOf szuka od końca. |
Console.Write(x) | nic | Wypisuje bez przechodzenia do nowej linii — podstawa budowania wierszy tabeli w pętli wewnętrznej. |
new string(znak, n) | string |
Napis z n powtórzeń znaku. Często zastępuje całą pętlę wewnętrzną. |
Math.Sqrt(x) | double |
Pierwiastek kwadratowy. W teście pierwszości wyznacza granicę, do której trzeba sprawdzać dzielniki. |
Environment.NewLine | string |
Znak (lub para znaków) końca linii właściwy dla systemu. Przydatny, gdy budujesz wielolinijkowy napis zamiast wypisywać go od razu. |
Liczby pierwsze do 50
Liczba pierwsza to taka większa od 1, która dzieli się bez reszty tylko przez 1 i przez samą siebie. Sprawdzenie wymaga pętli w pętli — i jest wzorcowym przykładem, w którym break naprawdę się opłaca.
const int Granica = 50;
int znalezione = 0;
Console.WriteLine($"Liczby pierwsze do {Granica}:");
Console.WriteLine();
for (int n = 2; n <= Granica; n++)
{
bool pierwsza = true;
// szukamy dzielnika; wystarczy sprawdzac do pierwiastka z n
for (int d = 2; d * d <= n; d++)
{
if (n % d == 0)
{
pierwsza = false;
break; // jeden dzielnik wystarczy, reszty nie sprawdzamy
}
}
if (!pierwsza)
{
continue; // n nie jest pierwsza - nastepna liczba
}
Console.Write($"{n,4}");
znalezione++;
if (znalezione % 10 == 0) // po dziesieciu liczbach lamiemy wiersz
{
Console.WriteLine();
}
}
Console.WriteLine();
Console.WriteLine();
Console.WriteLine($"Znaleziono {znalezione} liczb pierwszych.");
Liczby pierwsze do 50:
2 3 5 7 11 13 17 19 23 29
31 37 41 43 47
Znaleziono 15 liczb pierwszych.
Omówienie
| Fragment | Co robi i dlaczego tak |
|---|---|
bool pierwsza = true; |
Zakładamy niewinność. Pętla wewnętrzna szuka dowodu winy — jednego dzielnika. Gdy go nie znajdzie, flaga zostaje prawdziwa. To bardzo częsty wzorzec: „załóż, że tak, i szukaj kontrprzykładu”. |
d * d <= n |
Wystarczy sprawdzać dzielniki do pierwiastka z n. Gdyby n miało dzielnik większy od pierwiastka, to drugi czynnik byłby mniejszy — i znaleźlibyśmy go wcześniej. Zapis d * d <= n jest lepszy niż d <= Math.Sqrt(n): liczy na liczbach całkowitych, bez zaokrągleń i bez wywoływania metody w każdym obiegu. |
break po znalezieniu dzielnika |
Dla liczby 100 dzielnik znajdujemy przy d = 2. Bez break sprawdzalibyśmy jeszcze 3, 4, 5… aż do 10 — zupełnie bez sensu, bo odpowiedź już znamy. |
continue po nieudanym teście |
Alternatywą byłoby owinięcie trzech kolejnych instrukcji w if (pierwsza) { ... }. Wersja z continue jest płytsza i czyta się ją jak listę kroków: „nie jest pierwsza? następna”. |
Console.Write($"{n,4}") |
Write, nie WriteLine — wszystkie liczby jednego wiersza mają stać obok siebie. Szerokość 4 zapewnia równe kolumny także dla liczb dwucyfrowych. |
znalezione % 10 == 0 |
Łamanie wiersza co dziesiątą liczbę — po dziesiątej, dwudziestej i tak dalej. Sprawdzamy licznik znalezionych, a nie n, bo to znalezione trafiają do wiersza. |
| dwie liczby 2 i 3 | Warto prześledzić: dla n = 2 warunek 2 * 2 <= 2 jest od razu fałszywy, więc pętla wewnętrzna nie wykonuje się ani razu, flaga zostaje prawdziwa i dwójka trafia na listę. Dokładnie tak ma być — i to jest ta sytuacja, w której for wykonuje się zero razy. |
Ile obiegów oszczędza break
Dla granicy 50 różnica jest niezauważalna. Ale dla granicy miliona pętla wewnętrzna bez break wykonałaby się kilkaset razy dla każdej liczby parzystej, zamiast raz. To już różnica między sekundą a kilkoma minutami.
Na czym się najczęściej potykamy
| Błąd | Co się dzieje i jak poprawić |
|---|---|
break w switch ma zakończyć pętlę |
Kończy tylko switch. Użyj flagi sprawdzanej przez pętlę. |
break ma wyjść z dwóch pętli |
Wychodzi z najbliższej. Potrzebna flaga w warunku pętli zewnętrznej albo wydzielenie kodu do metody. |
continue przed i++ w while |
Pętla nieskończona — licznik nigdy nie rośnie. W while unikaj continue albo zwiększaj licznik na początku bloku. |
WriteLine w pętli wewnętrznej tabeli |
Każda wartość w osobnej linii. Wewnątrz Write, a WriteLine dopiero po pętli wewnętrznej. |
| ta sama nazwa licznika w obu pętlach | CS0136 — nie można zadeklarować i wewnątrz zasięgu, w którym i już istnieje. Używaj i, j, k albo nazw znaczących (wiersz, kolumna). |
kod po break w tym samym bloku |
CS0162: Unreachable code detected — ostrzeżenie o instrukcjach, do których program nigdy nie dojdzie. Zwykle oznacza, że break stoi za wcześnie. |
| zerowanie flagi w złym miejscu | bool pierwsza = true; musi być wewnątrz pętli zewnętrznej — flaga dotyczy jednej liczby. Postawiona przed pętlą zostałaby fałszywa po pierwszej liczbie złożonej i wszystkie kolejne uznano by za złożone. |
| niedoszacowanie liczby obiegów | Dwie pętle po 10 000 obiegów to sto milionów wykonań bloku wewnętrznego. Zanim uruchomisz — pomnóż zakresy. |
d <= n / 2 zamiast d * d <= n |
Działa poprawnie, ale robi znacznie więcej pracy. Dla liczby miliona: 500 000 sprawdzeń zamiast 1000. |
Zadania
Użytkownik podaje liczby. Sumuj je, ale gdy poda liczbę ujemną — przerwij i wypisz sumę bez niej. Maksymalnie dziesięć liczb.
pokaż rozwiązanie
double suma = 0;
int ile = 0;
for (int i = 1; i <= 10; i++)
{
Console.Write($"Liczba {i}/10 (ujemna konczy): ");
bool ok = double.TryParse(Console.ReadLine(), out double x);
if (!ok)
{
Console.WriteLine(" To nie liczba - pomijam.");
continue;
}
if (x < 0)
{
Console.WriteLine(" Liczba ujemna - koncze.");
break;
}
suma += x;
ile++;
}
Console.WriteLine();
Console.WriteLine($"Zsumowano {ile} liczb, suma = {suma:F2}");
Liczba 1/10 (ujemna konczy): 10
Liczba 2/10 (ujemna konczy): 5,5
Liczba 3/10 (ujemna konczy): abc
To nie liczba - pomijam.
Liczba 4/10 (ujemna konczy): 4
Liczba 5/10 (ujemna konczy): -1
Liczba ujemna - koncze.
Zsumowano 3 liczb, suma = 19,50
Obie instrukcje w jednej pętli i każda robi coś innego: continue pomija błędny wpis (ale zużywa jedną z dziesięciu prób), break kończy zbieranie. Liczba ujemna nie trafia do sumy, bo break stoi przed dodawaniem.
10 + 5,5 + 4 = 19,50.
Wypisz liczby od 1 do 30, pomijając te podzielne przez 3 lub przez 5. Na końcu podaj, ile liczb zostało wypisanych i ile pominiętych.
pokaż rozwiązanie
int wypisane = 0;
int pominiete = 0;
for (int i = 1; i <= 30; i++)
{
if (i % 3 == 0 || i % 5 == 0)
{
pominiete++;
continue;
}
Console.Write($"{i,4}");
wypisane++;
}
Console.WriteLine();
Console.WriteLine();
Console.WriteLine($"Wypisano: {wypisane}");
Console.WriteLine($"Pominieto: {pominiete}");
1 2 4 7 8 11 13 14 16 17 19 22 23 26 28 29
Wypisano: 16
Pominieto: 14
Liczby podzielne przez 3: 3, 6, 9, 12, 15, 18, 21, 24, 27, 30 — dziesięć. Przez 5: 5, 10, 15, 20, 25, 30 — sześć. Ale 15 i 30 są na obu listach, więc pominiętych jest 10 + 6 − 2 = 14, a wypisanych 30 − 14 = 16. To zasada włączeń i wyłączeń — warto ją znać, bo taki błąd w rachunku bywa trudny do wypatrzenia.
Wczytaj wysokość i narysuj trójkąt wyśrodkowany z gwiazdek: w wierszu i ma być 2·i − 1 gwiazdek, poprzedzonych spacjami tak, żeby trójkąt był symetryczny.
pokaż rozwiązanie
Console.Write("Wysokosc (1-20): ");
bool ok = int.TryParse(Console.ReadLine(), out int h);
if (!ok || h < 1 || h > 20)
{
Console.WriteLine("Podaj liczbe calkowita z zakresu 1-20.");
}
else
{
Console.WriteLine();
for (int i = 1; i <= h; i++)
{
// spacje przed gwiazdkami
for (int s = 1; s <= h - i; s++)
{
Console.Write(' ');
}
// gwiazdki
for (int g = 1; g <= 2 * i - 1; g++)
{
Console.Write('*');
}
Console.WriteLine();
}
}
*
***
*****
*******
*********
Dwie pętle wewnętrzne, jedna po drugiej, w jednym obiegu zewnętrznej: najpierw spacje, potem gwiazdki, a nowa linia dopiero po obu. Liczba spacji maleje (h - i), liczba gwiazdek rośnie (2 * i - 1) i suma zawsze daje tę samą szerokość.
Sprawdź na wierszu trzecim przy wysokości 5: spacji 5 - 3 = 2, gwiazdek 2 · 3 − 1 = 5.
Krócej: Console.WriteLine(new string(' ', h - i) + new string('*', 2 * i - 1)); — jedna linia zamiast dziewięciu. Warto umieć obie wersje.
Rozbuduj menu z lekcji 13 tak, żeby program wracał do niego po każdej operacji i kończył się dopiero po wybraniu opcji „koniec”. Policz, ile operacji wykonano.
pokaż rozwiązanie
bool dziala = true;
int operacje = 0;
while (dziala)
{
Console.WriteLine();
Console.WriteLine("=== MENU ===");
Console.WriteLine(" d - dodaj u - usun");
Console.WriteLine(" w - wypisz k - koniec");
Console.Write("Wybor: ");
string wpis = (Console.ReadLine() ?? "").Trim().ToLower();
if (wpis.Length == 0)
{
Console.WriteLine("Nic nie wybrano.");
continue;
}
switch (wpis[0])
{
case 'd':
Console.WriteLine("-> Dodawanie pozycji");
operacje++;
break;
case 'u':
Console.WriteLine("-> Usuwanie pozycji");
operacje++;
break;
case 'w':
Console.WriteLine("-> Wypisywanie listy");
operacje++;
break;
case 'k':
dziala = false;
break;
default:
Console.WriteLine($"Nieznana opcja: '{wpis[0]}'");
break;
}
}
Console.WriteLine();
Console.WriteLine($"Wykonano operacji: {operacje}. Do zobaczenia!");
=== MENU ===
d - dodaj u - usun
w - wypisz k - koniec
Wybor: d
-> Dodawanie pozycji
=== MENU ===
d - dodaj u - usun
w - wypisz k - koniec
Wybor: K
Wykonano operacji: 1. Do zobaczenia!
To jest ta pułapka z tej lekcji. W gałęzi 'k' nie może stać break zamiast dziala = false; — break wyszedłby tylko ze switch i menu wyświetliłoby się ponownie. Flaga jest jedynym prostym rozwiązaniem.
continue przy pustym wpisie jest bezpieczne, bo to pętla while bez licznika — nie ma czego pominąć. Gdyby pętla liczyła obiegi, trzeba by uważać.
Nieznana opcja nie zwiększa licznika operacji, bo nic się nie wydarzyło.
Znajdź wszystkie trójki liczb całkowitych a ≤ b ≤ c z zakresu 1–30, dla których a² + b² = c². Policz, ile ich jest i ile razy wykonał się najgłębszy blok.
pokaż rozwiązanie
const int Max = 30;
int trojki = 0;
int sprawdzen = 0;
for (int a = 1; a <= Max; a++)
{
for (int b = a; b <= Max; b++) // b zaczyna od a, wiec a <= b
{
for (int c = b; c <= Max; c++) // c zaczyna od b, wiec b <= c
{
sprawdzen++;
if (a * a + b * b == c * c)
{
Console.WriteLine($"{a,3}{b,4}{c,4}");
trojki++;
break; // dla tej pary (a,b) drugiego c nie bedzie
}
}
}
}
Console.WriteLine();
Console.WriteLine($"Trojek: {trojki}");
Console.WriteLine($"Sprawdzen: {sprawdzen}");
3 4 5
5 12 13
6 8 10
7 24 25
8 15 17
9 12 15
10 24 26
12 16 20
15 20 25
18 24 30
20 21 29
Trojek: 11
Sprawdzen: 4845
Rozpoczynanie b od a, a c od b to najważniejsza część tego rozwiązania. Gdyby wszystkie trzy pętle zaczynały od 1, program znalazłby trójkę (3, 4, 5) i osobno (4, 3, 5) — a to ta sama trójka. Wymuszenie porządku a ≤ b ≤ c w nagłówkach pętli jest krótsze i szybsze niż odsiewanie duplikatów później.
break w najgłębszej pętli działa, bo dla ustalonych a i b istnieje co najwyżej jedno c — dalsze sprawdzanie byłoby pewną stratą czasu.
Trzy pętle zagnieżdżone oznaczają liczbę sprawdzeń rosnącą z trzecią potęgą zakresu. Dla granicy 30 to około pięciu tysięcy, dla 300 byłoby ich już około pięciu milionów. Warto to sobie policzyć, zanim się uruchomi.
Wczytaj liczbę całkowitą większą od 1 i rozłóż ją na czynniki pierwsze, wypisując je w postaci iloczynu, np. 360 = 2 * 2 * 2 * 3 * 3 * 5.
pokaż rozwiązanie
Console.Write("Podaj liczbe wieksza od 1: ");
bool ok = int.TryParse(Console.ReadLine(), out int liczba);
if (!ok || liczba < 2)
{
Console.WriteLine("Podaj liczbe calkowita wieksza od 1.");
}
else
{
int reszta = liczba;
string wynik = "";
int czynnikow = 0;
for (int d = 2; d * d <= reszta; d++)
{
while (reszta % d == 0) // ten sam dzielnik moze wystapic wielokrotnie
{
wynik += (czynnikow == 0 ? "" : " * ") + d;
czynnikow++;
reszta /= d;
}
}
// co zostalo, jest juz liczba pierwsza (o ile nie jest jedynka)
if (reszta > 1)
{
wynik += (czynnikow == 0 ? "" : " * ") + reszta;
czynnikow++;
}
Console.WriteLine($"{liczba} = {wynik}");
Console.WriteLine($"Liczba czynnikow: {czynnikow}");
if (czynnikow == 1)
{
Console.WriteLine($"{liczba} jest liczba pierwsza.");
}
}
Podaj liczbe wieksza od 1: 360
360 = 2 * 2 * 2 * 3 * 3 * 5
Liczba czynnikow: 6
Podaj liczbe wieksza od 1: 97
97 = 97
Liczba czynnikow: 1
97 jest liczba pierwsza.
Pętla while wewnątrz pętli for. Dzielnik 2 mieści się w 360 aż trzy razy, więc trzeba go zdejmować, dopóki się da. Zwykłe if zdjęłoby tylko jedną dwójkę.
Dlaczego znalezione dzielniki na pewno są pierwsze? Idziemy od najmniejszych. Zanim dojdziemy do 4, wszystkie dwójki zostały już zdjęte, więc 4 nie podzieli reszty. Ten sam mechanizm eliminuje 6, 8, 9 i każdą inną liczbę złożoną — nie trzeba żadnego dodatkowego testu pierwszości.
Sprawdzenie po pętli jest konieczne. Dla liczby 97 warunek d * d <= reszta kończy pętlę przy d = 10, a w reszta nadal siedzi 97. Bez tej linii program nie wypisałby nic. Ten sam przypadek zachodzi dla 2 · 97 = 194: po zdjęciu dwójki zostaje 97, którego pętla już nie dosięgnie.
Uwaga na warunek d * d <= reszta: porównujemy z resztą, która maleje w trakcie działania — nie z pierwotną liczbą. Dzięki temu pętla kończy się dużo wcześniej.
Operator warunkowy czynnikow == 0 ? "" : " * " dokleja separator przed każdym czynnikiem oprócz pierwszego. To standardowa sztuczka na budowanie list rozdzielanych przecinkiem; w bloku o tablicach zastąpi ją metoda string.Join.
Co trzeba zapamiętać
breakkończy całą pętlę,continuetylko bieżący obieg.- Obie instrukcje dotyczą najbliższej otaczającej pętli —
breakwswitchkończyswitch, nie pętlę. - Do wyjścia z dwóch pętli naraz służy flaga sprawdzana w warunku pętli zewnętrznej.
continuew pętliwhilepotrafi pominąć zwiększenie licznika i zawiesić program; wforlicznik rośnie zawsze.- W pętlach zagnieżdżonych liczba wykonań bloku wewnętrznego to iloczyn zakresów.
- W tabelach:
Writew pętli wewnętrznej,WriteLinepo niej. - Flagę w rodzaju
bool pierwsza = true;zeruj wewnątrz pętli zewnętrznej, nie przed nią. - W teście pierwszości sprawdzaj dzielniki do pierwiastka — warunkiem
d * d <= n. - Jeden
breakw pętli to zwykle dobrze; trzy oznaczają, że pętlę trzeba przemyśleć.
Koniec bloku 3
Twoje programy potrafią już podejmować decyzje i powtarzać czynności — to komplet narzędzi sterowania przebiegiem. Brakuje jednego: wszystkie dane wciąż trzymasz w osobnych zmiennych. W następnym bloku poznasz tablice i wtedy pętle pokażą pełnię swoich możliwości.