Bilgisayar ve Programlamaya Giriş · Ders Notu
Döngüler
Bilgisayarın asıl gücü, aynı işi yorulmadan binlerce kez yapabilmesinden gelir. Bir programda bu tekrarı döngüler sağlar. Not, önce her döngüde bulunan dört parçayı tanıtır: ilk değer verme, koşul, gövde ve güncelleme. Ardından Java'nın üç döngüsü gelir: for, while ve do-while. Her biri küçük bir programla tur tur izlenir. Döngünün akışını değiştiren break ve continue deyimleri, sonsuz döngü ve sınır hataları gibi tipik yanlışlar da ayrıntılı ele alınır. Notun ikinci yarısı örneklere ayrılmıştır. Sayının rakamlarıyla çalışan döngüler, bölen arayan döngüler, kullanıcıdan girdi okuyan döngüler ve iç içe döngülerle çizilen desenler birkaç ortak kalıpta toplanır. Bütün Java programları JDK 21 ile derlenip çalıştırılmıştır; notta verilen çıktılar bu çalıştırmalardan alınmıştır.
Öğrenme çıktıları
- Döngü
- Bir döngünün ilk değer verme, koşul, gövde ve güncelleme parçalarını kodda göstermek;
for,whilevedo-whilearasından probleme uygun olanı seçmek;break,continueve etiketlibreakdeyimlerinin akışı nasıl değiştirdiğini açıklamak. - Kalıplar
- Sayaç ve biriktirici kullanarak toplam, çarpım ve sayım yapmak; rakam kalıbıyla (
% 10,/ 10) basamak sayısı, rakamlar toplamı, ters çevirme ve palindrom problemlerini çözmek; bölen kalıbıyla asal ve mükemmel sayıları bulmak. - İç içe
- İç içe döngülerle satır-sütun düzenleri ve yıldız ya da sayı desenleri üretmek; bir desenin her satırındaki boşluk ve karakter sayısını satır numarasına bağlayan formülü bulmak.
- İzleme
- Bir döngüyü izleme tablosuyla tur tur yürütmek; sonsuz döngü, sınır hatası ve
doublesayaç hatası gibi yanlışları bulmak; döngüyü normal, sıfır, bir, negatif ve uç değerlerle test etmek.
8.1Döngü nedir?
8.1.1Tekrar neden gerekir?
1'den 4'e kadar sayıların toplamını yazdıran bir program düşünelim. Dört satırlık bir çözüm akla hemen gelir: toplam = 1 + 2 + 3 + 4;. Aynı iş 1'den 1000'e kadar istendiğinde bu yol işlemez. Bin terimlik bir satır yazmak hem zahmetlidir hem de hataya açıktır. Üstelik üst sınır çoğu zaman program yazılırken bilinmez; kullanıcı onu çalışma sırasında girer.
Bu problemlerin ortak yanı, aynı küçük işin art arda yapılmasıdır: “toplama bir sayı daha ekle, sonra sıradaki sayıya geç”. Programlama dillerinde bu tür tekrarı anlatan yapıya döngü denir. Programcı tekrarlanacak işi bir kez yazar ve ne zaman duracağını söyler. Gerisini bilgisayar yapar.
Döngü (loop), bir grup deyimi bir koşul doğru olduğu sürece tekrar tekrar çalıştıran yapıdır. Tekrarlanan deyimlere döngü gövdesi (loop body), gövdenin bir kez çalışmasına tur (iteration) denir.
Java'da üç döngü vardır: for, while ve do-while. Üçü de aynı işi yapabilir; aralarındaki fark, koşulun ne zaman sorulduğu ve parçaların nereye yazıldığıdır. Bu yüzden önce her döngüde ortak olan parçaları tanımak gerekir.
8.1.2Döngünün dört parçası
Her döngü dört parçadan oluşur. Sunumda ve bu notta her parça hep aynı renkle gösterilir. Şekil 8.1 bu parçaları genel bir akış diyagramında toplar.
- İlk değer verme (initialization): döngüden önce bir kez çalışır ve döngünün kullanacağı değişkenlere başlangıç değeri verir. Şekildeki
sayac = 1böyle bir adımdır. - Koşul: sonucu
trueya dafalseolan bir mantıksal ifadedir.forvewhiledöngülerinde her turdan önce,do-whiledöngüsünde her turdan sonra sorulur. Sonuçfalseise döngüden çıkılır. - Gövde: her turda yapılan iştir. Şekilde “işi yap” kutusuyla gösterilmiştir.
- Güncelleme: koşulda kullanılan değişkeni değiştirir. Örneğin
sayac++sayacı 1 artırır. Güncellemenin görevi koşulu her turda sona biraz daha yaklaştırmaktır. Güncelleme unutulursa koşul hep aynı kalır ve döngü bitmez.
Şekildeki koşul sayac <= n ? sorusudur. sayac 1'den başlar ve her turda 1 artar. Bir gün n'yi geçer ve koşul yanlış olur. Döngünün bitmesini güvenceye alan şey bu ilerlemedir. Bir döngü yazılırken dört parçanın her birinin nerede olduğu açıkça görülebilmelidir. Parçalardan biri eksik ya da yanlışsa döngü ya hiç çalışmaz ya bir tur eksik/fazla çalışır ya da hiç bitmez.
Sayaç (counter), her turda sabit bir miktar (çoğunlukla 1) değişen ve kaç tur döndüğünü ya da sıradaki elemanı gösteren değişkendir. Biriktirici (accumulator), her turda yeni bir değeri kendisine ekleyen ya da kendisiyle çarpan değişkendir. Toplam biriktiricisi 0'dan, çarpım biriktiricisi 1'den başlar.
Başlangıç değerlerinin nedeni basittir. 0, toplamada etkisiz elemandır: 0'a bir sayı eklemek o sayıyı verir. Çarpmada etkisiz eleman 1'dir. Çarpım biriktiricisi yanlışlıkla 0 ile başlatılırsa her tur 0 ile çarptığı için sonuç hep 0 çıkar.
8.2for döngüsü
8.2.1Yazım ve çalışma sırası
Tekrar sayısı baştan belli olduğunda en uygun döngü for döngüsüdür. Dört parçanın üçü, yani ilk değer verme, koşul ve güncelleme, aynı satırda, for sözcüğünden sonraki parantezin içinde yazılır. Parçalar noktalı virgülle ayrılır. Gövde süslü parantezlerin arasına gelir. Aşağıdaki program 1'den n'ye kadar sayıları toplar:
int n = 4;
int toplam = 0;
for (int i = 1; i <= n; i++) {
toplam += i;
}
System.out.println(toplam);Program çalıştırıldığında ekrana yalnızca toplam yazılır:
10int i = 1 ilk değer verme, i <= n koşul, i++ güncellemedir. Gövde toplam += i deyimidir.Şekil 8.2 bu programın akışını gösterir. for döngüsünün parçaları kodda yan yana yazılsa da yazıldıkları sırayla çalışmaz. Gerçek çalışma sırası şöyledir:
int i = 1çalışır. Bu adım yalnızca bir kez, döngüye girerken yapılır.- Koşul
i <= nsorulur. Doğruysa gövdeye girilir, yanlışsa döngü biter. - Gövde
toplam += içalışır. - Güncelleme
i++çalışır ve akış 2. adıma, yani koşula döner.
Kısaca sıra “ilk değer verme → koşul → gövde → güncelleme → koşul → gövde → güncelleme …” biçimindedir. Güncelleme parantezin içinde gövdeden önce yazılır ama her zaman gövdeden sonra çalışır. Bu ayrıntı döngüyü elle izlerken çok önemlidir.
Başlıkta tanımlanan i değişkeninin bir özelliği daha vardır: yalnızca döngünün içinde yaşar. Bir değişkenin kullanılabildiği program bölgesine kapsam (scope) denir. for (int i = 1; …) biçiminde tanımlanan i'nin kapsamı döngünün kendisidir. Döngüden sonra System.out.println(i); yazılırsa derleyici i'yi tanımaz ve hata verir. Döngüden sonra i'nin değeri gerekiyorsa i döngüden önce tanımlanmalıdır: int i; for (i = 1; i <= n; i++) ….
8.2.2for döngüsünü adım adım izlemek
Bir döngünün ne yaptığını anlamanın en güvenilir yolu, onu kâğıt üzerinde bilgisayar gibi çalıştırmaktır. Her turda değişkenlerin aldığı değerler bir tabloya yazılır. Bu tabloya izleme tablosu (trace table) denir. Tablo 8.1 yukarıdaki programın izleme tablosudur. Tablo, programın aynısı JDK 21 ile çalıştırılıp her turda değişkenler yazdırılarak üretilmiştir.
| Tur | i | i <= n | toplam (gövdeden sonra) |
|---|---|---|---|
| 1 | 1 | true | 1 |
| 2 | 2 | true | 3 |
| 3 | 3 | true | 6 |
| 4 | 4 | true | 10 |
| — | 5 | false | 10 (döngü biter) |
Tablo şöyle okunur. Döngüye girilirken int i = 1 çalışır. 1. turda koşul 1 <= 4 doğrudur ve gövde toplam = 0 + 1 = 1 yapar. Ardından i++ ile i 2 olur ve akış koşula döner. 2. turda 2 <= 4 doğrudur ve toplam = 1 + 2 = 3 olur. Aynı biçimde 3. turda toplam 6, 4. turda 10 olur. 4. turun sonundaki i++ i'yi 5 yapar. Koşul 5 <= 4 yanlıştır ve döngü biter. Döngüden sonraki println 10 yazar.
Tablodan iki sayım çıkar. Gövde 4 kez çalıştı, koşul ise 5 kez soruldu. Koşulun son, yanlış sonuçlanan denetimi de bir değerlendirmedir. Genel olarak, i 1'den n'ye kadar artan bir for döngüsünde gövde n kez, koşul n + 1 kez çalışır. İlk değer verme ise yalnızca 1 kez çalışır.
Biriktiriciyi döngünün içinde tanımlamak. int toplam = 0; satırı gövdenin içine taşınırsa iki sorun çıkar. Birincisi, her tur toplamı yeniden 0 yapar ve önceki turların emeği kaybolur. İkincisi, toplamın kapsamı gövdeyle sınırlı kalır ve döngüden sonraki println(toplam) derlenmez. Biriktirici her zaman döngüden önce tanımlanır ve başlangıç değerini orada alır.
8.2.3for başlığının esnek biçimleri
for başlığının üç parçası her zaman “1'den başla, 1 artır” biçiminde olmak zorunda değildir. Sayaç herhangi bir değerden başlayabilir, ikişer artabilir ya da geriye doğru sayabilir. İlk değer verme bölümünde aynı tipte birden çok değişken tanımlanabilir; güncelleme bölümüne de virgülle ayrılmış birden çok ifade yazılabilir. Tablo 8.2 üç örnek başlığı ve ürettikleri değerleri verir.
| Başlık | Çalışma |
|---|---|
for (int i = 0, j = 9; i < j; i++, j--) | i ve j birlikte ilerler: (0,9) (1,8) (2,7) (3,6) (4,5); 5 tur |
for (int i = 0; i <= 10; i += 2) | 0 2 4 6 8 10 |
for (int i = 5; i >= 1; i--) | geri sayım: 5 4 3 2 1 |
İlk satırda iki sayaç vardır: i baştan, j sondan ilerler. Her turda i 1 artar, j 1 azalır. Koşul i < j, iki sayaç karşılaştığında döngüyü durdurur. (4,5) turundan sonra i = 5 ve j = 4 olur; 5 < 4 yanlıştır. Bu kalıp, bir dizinin iki ucundan ortaya doğru ilerleyen algoritmalarda sık kullanılır.
Üç bölümün her biri isteğe bağlıdır. Hatta üçü birden boş bırakılabilir. Koşul bölümü boşsa Java onu true sayar ve döngü kendiliğinden bitmez. Böyle bir döngüde çıkışı gövdedeki bir break sağlamalıdır:
int k = 0;
for (;;) { // üç bölüm de boş: koşul true sayılır
k++;
if (k == 3) break; // çıkış koşulu şart
}
System.out.println(k); // 3k her turda 1 artar ve 3 olduğunda break döngüyü bitirir. Program 3 yazar. Bölümlerin boş bırakılabilmesi bir kolaylıktır ama okunabilirliği azaltır. Sayaçlı bir iş için üç bölümü dolu, sıradan bir for başlığı her zaman daha anlaşılırdır.
8.2.4i++ ile ++i
Java'da bir değişkeni 1 artırmanın iki kısa yolu vardır: i++ (sonra artırma) ve ++i (önce artırma). İkisi de i'yi 1 artırır. Fark, ifadenin değerindedir. i++ ifadesi i'nin eski değerini verir, sonra i'yi artırır. ++i önce i'yi artırır, sonra yeni değeri verir. Tablo 8.3 bu farkı dört küçük örnekle gösterir.
| Başlangıç | İfade | Sonuç | Sonra |
|---|---|---|---|
| a = 5 | b = a++ | b = 5 | a = 6 |
| a = 5 | b = ++a | b = 6 | a = 6 |
| idx = 0 | d[idx++] | 10 | idx = 1 |
| idx = 0 | d[++idx] | 20 | idx = 1 |
İlk iki satırda a her iki durumda da 6 olur; değişen yalnızca b'ye giden değerdir. Son iki satırda idx her iki durumda da 1 olur. d[idx++] eski indeksi (0) kullanır ve 10'u verir. d[++idx] önce indeksi 1'e çıkarır ve 20'yi verir.
for başlığının güncelleme bölümünde ise ifadenin değeri hiçbir yerde kullanılmaz; yalnızca yan etkisi, yani i'nin artması önemlidir. Bu yüzden aşağıdaki iki döngü arasında hiçbir fark yoktur:
for (int i = 0; i < 3; i++) System.out.print(i + " "); // 0 1 2
for (int i = 0; i < 3; ++i) System.out.print(i + " "); // 0 1 2İkisi de 0 1 2 yazar. Okunabilirlik açısından sayaçlarda alışılmış biçim olan i++ tercih edilir. İki yazım arasındaki fark ancak artırma ifadesinin değeri başka bir ifadenin içinde kullanıldığında ortaya çıkar. Bu tür iç içe kullanımlar kodu okumayı zorlaştırdığı için de kaçınılması gereken bir alışkanlıktır.
8.3while döngüsü
8.3.1Yazım ve çalışma sırası
for döngüsü tekrar sayısı belli olduğunda rahat bir yazım sunar. Bazı problemlerde ise kaç tur gerektiği baştan bilinmez. Bir sayının rakamlarını tek tek işlemek böyledir: 7 için bir, 725 için üç tur gerekir. Kullanıcı doğru tahmini yapana kadar soru sormak da böyledir. Bu durumlarda while döngüsü daha doğal bir seçimdir. while sözcüğünden sonra parantez içinde yalnızca koşul yazılır. İlk değer verme döngüden önce, güncelleme gövdenin içinde yer alır.
while döngüsü, while (koşul) { gövde } biçiminde yazılan ve koşulu her turdan önce soran döngüdür. Koşul doğru olduğu sürece gövde çalışır. Koşulu gövdeden önce sorulan döngülere ön koşullu döngü (pre-test loop) denir; for döngüsü de ön koşulludur.
Aşağıdaki program 5! değerini, yani 5 × 4 × 3 × 2 × 1 çarpımını hesaplar:
int sayi = 5;
int faktoriyel = 1;
while (sayi > 1) {
faktoriyel *= sayi;
sayi--;
}
System.out.println(faktoriyel);sayi = 5, faktoriyel = 1) döngüden önce, güncelleme (sayi--) gövdenin sonunda yer alır.Şekil 8.4 for diyagramıyla aynı biçimdedir. Değişen yalnızca kodun yazılışıdır: for döngüsünde başlıkta toplanan parçalar burada üç ayrı yere dağılmıştır. Bu dağınıklık while döngüsünün tek riskidir. Güncelleme gövdenin içinde unutulabilir. Böyle olursa koşul hiç değişmez ve döngü bitmez.
Koşul gövdeden önce sorulduğu için bir while döngüsünün gövdesi hiç çalışmayabilir. Yukarıdaki programda sayi = 1 verilirse ilk denetim 1 > 1 yanlış olur. Gövdeye hiç girilmez ve 1 yazılır. Bu sonuç doğrudur, çünkü 1! = 1'dir. Aynı biçimde sayi = 0 için de 1 yazılır ve 0! = 1 tanımıyla uyuşur.
8.3.25!'i adım adım hesaplamak
| Tur | sayi (koşulda) | sayi > 1 | faktoriyel | sayi (güncellemeden sonra) |
|---|---|---|---|---|
| 1 | 5 | true | 5 | 4 |
| 2 | 4 | true | 20 | 3 |
| 3 | 3 | true | 60 | 2 |
| 4 | 2 | true | 120 | 1 |
| — | 1 | false | 120 | — |
Tablo 8.4 her turu bir satırda gösterir. Çarpım biriktiricisi faktoriyel 1'den başlar. 1. turda 5 > 1 doğrudur; faktoriyel = 1 × 5 = 5 olur ve sayi-- sayi'yi 4'e indirir. 2. turda 5 × 4 = 20, 3. turda 20 × 3 = 60, 4. turda 60 × 2 = 120 hesaplanır. 4. turun sonunda sayi 1 olur. Koşul 1 > 1 yanlıştır ve döngü biter. Program 120 yazar.
Gövde 4 kez çalıştı. 1 ile çarpmak sonucu değiştirmediği için koşul sayi > 1 seçilmiştir; sayi >= 1 da aynı sonucu verirdi, yalnızca bir tur fazla dönerdi. Burada sayaç azalarak ilerler. Güncelleme sayi-- olduğu için sayi her turda 1 azalır ve sonunda 1'e iner. Döngünün bitmesini sağlayan bu ilerlemedir.
Aynı hesap for ile de yazılabilir: for (int i = 2; i <= 5; i++) faktoriyel *= i;. İki yazım da doğrudur. Tekrar sayısı baştan belli olduğu için for burada biraz daha okunaklıdır. Faktöriyel çok hızlı büyür: 12! = 479 001 600 int'e sığar, 13! = 6 227 020 800 sığmaz. int en çok 2 147 483 647 tutabilir. 13! int ile hesaplanırsa Java hata vermez; değer sessizce sarar ve 1932053504 gibi yanlış bir sayı yazılır. Bu yüzden faktöriyel hesabında long kullanılır. long 20!'e kadar yeter; 21! long'a da sığmaz.
8.4do-while döngüsü
8.4.1Yazım ve çalışma sırası
Bazı işlerde gövdenin en az bir kez çalışması gerekir. Bir menü programı düşünelim. Menü ekrana yazılmadan kullanıcı seçim yapamaz; seçim yapılmadan da “çıkılsın mı?” sorusunun cevabı bilinmez. Bu tür işler için Java'da do-while döngüsü vardır.
do-while döngüsü, do { gövde } while (koşul); biçiminde yazılan ve koşulu her turdan sonra soran döngüdür. Gövde koşula bakılmadan bir kez çalışır; sonraki turlar koşula bağlıdır. Böyle döngülere son koşullu döngü (post-test loop) denir.
Aşağıdaki program kullanıcıdan bir seçim okur. Seçim 1 ise “Merhaba!” yazar. Kullanıcı 0 girene kadar bu işi tekrarlar:
Scanner klavye = new Scanner(System.in);
int secim;
do {
System.out.print("Seçim (1/0): ");
secim = klavye.nextInt();
if (secim == 1)
System.out.println("Merhaba!");
} while (secim != 0);
System.out.println("Güle güle");Klavyeden okuma için Scanner sınıfı kullanılır. Dosyanın başına import java.util.Scanner; satırı yazılmalıdır. klavye.nextInt() çağrısı kullanıcı bir tam sayı girip Enter tuşuna basana kadar bekler ve girilen sayıyı döndürür.
secim = nextInt()) koşuldan önce gelir. Koşul doğruysa ok yukarı, gövdenin başına döner.Şekil 8.5 ile for ve while diyagramları karşılaştırıldığında fark hemen görülür: karar kutusu döngünün başında değil, sonundadır. secim değişkeni döngüden önce bildirilmiş ama değer almamıştır. Java, değer verilmemiş bir yerel değişkenin okunmasına izin vermez. Burada sorun çıkmaz, çünkü koşul ilk kez gövdeden sonra sorulur ve o ana kadar secim = klavye.nextInt(); satırı mutlaka çalışmıştır.
Yazımda iki ayrıntı dikkat ister. Birincisi, while (secim != 0) satırının sonunda noktalı virgül vardır; for ve while döngülerinde bu yerde noktalı virgül olmaz. İkincisi, koşulda kullanılan değişken döngüden önce bildirilmelidir. secim gövdenin içinde bildirilseydi kapsamı gövdeyle sınırlı kalır ve while satırında kullanılamazdı.
8.4.2Menü döngüsünü izlemek
Kullanıcı sırayla 1, 2 ve 0 girsin. Tablo 8.5 döngünün üç turunu gösterir.
| Tur | Okunan secim | secim == 1 | Yazılan | secim != 0 |
|---|---|---|---|---|
| 1 | 1 | true | Seçim (1/0): ve Merhaba! | true |
| 2 | 2 | false | Seçim (1/0): | true |
| 3 | 0 | false | Seçim (1/0): | false |
1. turda gövdeye koşula bakılmadan girilir. Menü yazılır, kullanıcı 1 girer, secim == 1 doğrudur ve “Merhaba!” yazılır. Turun sonunda 1 != 0 doğrudur ve gövdenin başına dönülür. 2. turda 2 okunur. if koşulu yanlıştır ve hiçbir şey yazılmaz; 2 != 0 doğru olduğu için döngü sürer. 3. turda 0 okunur ve 0 != 0 yanlıştır. Döngü biter ve “Güle güle” yazılır.
Gövde 3 kez çalıştı, koşul da 3 kez soruldu. Ön koşullu bir döngüde koşul, gövdeden bir fazla sorulur. Son koşullu döngüde ikisinin sayısı eşittir. Program aynı girdilerle JDK 21'de çalıştırıldığında aşağıdaki çıktıyı verir. Girdiler bir dosyadan yönlendirildiği için kullanıcının yazdığı sayılar ekranda görünmez; istem satırları bu yüzden yan yana düşmüştür:
Seçim (1/0): Merhaba!
Seçim (1/0): Seçim (1/0): Güle güleKullanıcı ilk seçimde 0 girerse çıktı yalnızca bir istem satırı ve “Güle güle”den oluşur. Menü yine de bir kez yazılmıştır. do-while döngüsünün doğal kullanım alanı budur: menüler ve girdi doğrulama. Örneğin kullanıcıdan 1 ile 5 arasında bir not istenirken soru en az bir kez sorulmalıdır. Girilen değer aralık dışındaysa soru tekrarlanır: do { not = sc.nextInt(); } while (not < 1 || not > 5);. Girdi 7, 0, 3 olduğunda döngü üç tur döner ve not = 3 ile çıkar.
Gövdenin hiç çalışmaması gereken bir yerde do-while kullanmak. Koşul ilk denemede yanlışsa while ve do-while farklı davranır. Aşağıdaki iki döngüde koşul baştan yanlıştır (n = m = 5, sınır 3). do-while yine de bir kez çalışır, 5 yazar ve n'yi 6 yapar. while döngüsünün gövdesi hiç çalışmaz ve m 5 kalır. Çıktı 5 | 6 5 olur. Gövdenin sıfır kez çalışabileceği her durumda ön koşullu döngü seçilir.
int n = 5;
do {
System.out.print(n + " ");
n++;
} while (n < 3);
int m = 5;
while (m < 3) {
System.out.print(m + " ");
m++;
}
System.out.println("| " + n + " " + m);8.4.3Hangi döngü seçilir?
Üç döngü birbirine çevrilebilir. Her for döngüsü bir while döngüsüyle, her while döngüsü de bir for döngüsüyle yazılabilir. do-while da bir while döngüsü ve önüne eklenen bir gövde kopyasıyla yazılabilir. Yani seçim bir yetenek meselesi değil, okunabilirlik meselesidir. Doğru döngü, okuyana “bu döngü ne zaman ve kaç kez döner?” sorusunun cevabını en kısa yoldan verir.
| Durum | Tercih |
|---|---|
| 1'den n'ye say, n kez tekrarla | for |
| Sayı 0 olana kadar basamak işle | while |
| Menü göster, geçerli girdi gelene kadar sor | do-while |
| Kullanıcı “0” yazana kadar topla | while / do-while |
Son satırda iki seçenek vardır, çünkü iki çözüm de doğal görünür. while ile yazılan çözüm ilk sayıyı döngüden önce okur, döngünün sonunda bir sonrakini okur. do-while ile yazılan çözüm okumayı gövdenin başına koyar ama 0'ın toplama eklenmemesi için gövdede ayrıca bir denetim ister. Hangisi daha açık görünüyorsa o seçilir.
8.5break ve continue
8.5.1Turu atlamak ve döngüden çıkmak
Bir döngü normalde yalnızca koşulu yanlış olduğunda biter ve her turda gövdenin tamamı çalışır. İki deyim bu düzeni değiştirir.
break, içinde bulunduğu en yakın döngüyü hemen bitirir; akış döngüden sonraki ilk deyimden sürer. continue, yalnızca o turun kalanını atlar. for döngüsünde akış güncellemeye (i++), while ve do-while döngüsünde koşula geçer; döngü sürer.
Aşağıdaki döngü 1'den 10'a kadar sayar. 3'ün katlarını atlar ve 8'e gelince durur:
for (int i = 1; i <= 10; i++) {
if (i % 3 == 0) continue;
if (i == 8) break;
System.out.print(i + " ");
}
System.out.println("| bitti");print(i) kutusunu atlayıp doğrudan i++'ya gider. break oku döngünün dışına, “döngü sonu”na çıkar.8.5.2break ve continue'yu izlemek
| i | i % 3 == 0 | i == 8 | Sonuç | Çıktı |
|---|---|---|---|---|
| 1 | false | false | 1 yazılır | 1 |
| 2 | false | false | 2 yazılır | 1 2 |
| 3 | true | sorulmaz | continue: yazdırma atlanır | 1 2 |
| 4 | false | false | 4 yazılır | 1 2 4 |
| 5 | false | false | 5 yazılır | 1 2 4 5 |
| 6 | true | sorulmaz | continue: yazdırma atlanır | 1 2 4 5 |
| 7 | false | false | 7 yazılır | 1 2 4 5 7 |
| 8 | false | true | break: döngü biter | 1 2 4 5 7 |
Tablo 8.7 her i için iki denetimin sonucunu verir. i = 1, 2, 4, 5, 7 için iki koşul da yanlıştır ve sayı yazılır. i = 3 ve i = 6 için i % 3 == 0 doğrudur. continue çalışır, print atlanır ve akış doğrudan i++'ya geçer. Bu turlarda ikinci if hiç sorulmaz. i = 8'de ilk koşul yanlıştır (8 % 3 = 2), ikincisi doğrudur ve break döngüyü bitirir. 9 ve 10 hiç denenmez. Akış döngüden sonraki satırdan sürer ve çıktı şöyle olur:
1 2 4 5 7 | bittiİki deyimin sırası da önemlidir. Aynı yapıda koşullar i % 4 == 0 ve i == 10 olsaydı ve döngü 12'ye kadar sürseydi, 4 ve 8 atlanır, 10'da break çalışırdı. Çıktı 1 2 3 5 6 7 9 | son olurdu. Bu döngüde i = 8 için önce continue denetimi yapıldığından, break'e o turda hiç sıra gelmez.
break ve continue döngüleri kısaltabilir ama akışı izlemeyi de zorlaştırır. Gövdenin ortasından çıkan bir döngüde, döngünün ne zaman bittiğini anlamak için başlığa bakmak yetmez; bütün gövdenin okunması gerekir. Bu yüzden bu deyimler ölçülü kullanılır. Aynı iş bir koşulla ya da bir bayrak değişkeniyle açıkça yazılabiliyorsa o yol tercih edilir. Asal sayı testindeki asal && i <= n / i koşulu böyle bir örnektir.
8.5.3Etiketli break
Düz break yalnızca içinde bulunduğu en yakın döngüyü bitirir. İç içe iki döngüde iç döngüdeki bir break, dış döngüyü durdurmaz. İki döngüden birden çıkmak gerektiğinde Java'nın etiketli break (labeled break) deyimi kullanılır. Dış döngünün önüne bir ad ve iki nokta yazılır; break ad; o adı taşıyan döngüyü bitirir.
dis:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (i * j == 4) break dis;
System.out.println(i + " " + j);
}
}
System.out.println("bitti");Program 1'den 3'e kadar i ve j çiftlerini yazar, çarpımı 4 olan ilk çiftte durur. Tablo 8.8 aynı döngünün etiketli ve düz break ile nasıl davrandığını yan yana gösterir.
| i | j | i * j | break dis ile | düz break ile |
|---|---|---|---|---|
| 1 | 1 | 1 | “1 1” yazılır | “1 1” yazılır |
| 1 | 2 | 2 | “1 2” yazılır | “1 2” yazılır |
| 1 | 3 | 3 | “1 3” yazılır | “1 3” yazılır |
| 2 | 1 | 2 | “2 1” yazılır | “2 1” yazılır |
| 2 | 2 | 4 | iki döngüden çıkılır | iç döngü biter |
| 2 | 3 | 6 | denenmez | denenmez |
| 3 | 1 | 3 | denenmez | “3 1” yazılır |
| 3 | 2 | 6 | denenmez | “3 2” yazılır |
| 3 | 3 | 9 | denenmez | “3 3” yazılır |
i = 1 iken j = 1, 2, 3 için çarpım 4 değildir ve üç satır yazılır. i = 2, j = 1 için çarpım 2'dir ve “2 1” yazılır. i = 2, j = 2'de 2 · 2 = 4 olur. Etiketli break dis iki döngüden birden çıkar; i = 3 hiç denenmez. Akış dış döngünün ardındaki satıra atlar ve “bitti” yazılır. Çıktı 4 sayı satırı ve “bitti”dir:
1 1
1 2
1 3
2 1
bittiEtiket kaldırılıp düz break yazılırsa i = 2, j = 2'de yalnızca iç döngü biter. Dış döngü i++ ile 3'e geçer ve iç döngü j = 1'den yeniden başlar. 3 için çarpımlar 3, 6, 9'dur; hiçbiri 4 değildir ve üç satır daha yazılır. Düz break ile 7 sayı satırı çıkar:
1 1
1 2
1 3
2 1
3 1
3 2
3 3
bittiEtiketli break, iç içe döngülerde bir şey arandığında ve bulunduğu anda bütün aramanın bitmesi gerektiğinde işe yarar. Aynı iş bir bayrakla da yapılabilir: bulununca bulundu = true yapılır ve dış döngünün koşuluna !bulundu && eklenir. Etiket adı herhangi bir geçerli Java adı olabilir; yaygın seçimler dis ya da arama gibi işi anlatan adlardır.
8.6Döngü hataları ve doğrulama
Döngü hataları çoğu zaman derleyiciye takılmaz. Program derlenir, çalışır ve yanlış bir sonuç verir ya da hiç bitmez. Bu kesimde dört tipik sorun ele alınır: sonsuz döngü, double sayaç, döngü değişmezinin bozulması ve sınır hataları.
8.6.1Sonsuz döngü
Sonsuz döngü (infinite loop), koşulu hiçbir zaman yanlış olmadığı için kendiliğinden bitmeyen döngüdür. Program “donmuş” görünür: ya hiçbir şey yazmaz ya da aynı satırları durmadan yazar.
i < 5 iken sayaç i-- ile 0, −1, −2, … diye azalır ve hedeften uzaklaşır. Alttaki while döngüsünde güncelleme hiç yoktur ve i hep 0 kalır.Şekil 8.9 iki tipik nedeni gösterir. İlkinde güncelleme yanlış yöndedir. for (int i = 0; i < 5; i--) döngüsünde i her turda azalır ve i < 5 hep doğru kalır. İkincisinde güncelleme hiç yazılmamıştır. while (i < 5) { … } gövdesinde i'yi değiştiren bir deyim yoksa koşul ilk turdaki değerini korur.
Birinci döngü kuramsal olarak sonsuzdur. Gerçekte ise int sınırlı bir tiptir. i, −2 147 483 648'e indikten sonra bir kez daha azaltılınca taşma (overflow) olur ve değer en büyük int'e, 2 147 483 647'ye sarar. Bu değer 5'ten küçük değildir ve döngü biter. JDK 21 ile sayılan tur sayısı aşağıdadır:
| Döngü | Tur sayısı | Son işlenen i |
|---|---|---|
| for (int i = 0; i < 5; i--) | 2147483649 | −2147483648 |
i, 0'dan −2 147 483 648'e kadar her değeri bir kez alır. Bu, 231 + 1 = 2 147 483 649 tur, yani yaklaşık 2,1 milyar turdur. Modern bir bilgisayar bu kadar turu birkaç saniyede bitirebilir; gövdede ekrana yazma varsa saatler sürer. Pratikte bu döngü de sonsuz sayılır ve doğru bir program değildir.
Bazen döngü bilerek sonsuz yazılır: while (true). Sunucular, oyun döngüleri ve bekçi değerle okuma bu biçimi kullanır. Böyle bir döngünün gövdesinde mutlaka bir break koşulu olmalıdır. Program bir sonsuz döngüye takılırsa komut satırında Ctrl+C tuşlarıyla durdurulur; geliştirme ortamlarında bunun için bir durdurma düğmesi vardır.
Süslü parantezleri unutmak. int i = 0; while (i < 5) System.out.print(i); i++; satırında gövde yalnızca print(i) deyimidir. Girinti ya da aynı satırda yazılmış olmak, i++'yı gövdeye katmaz. i hep 0 kalır ve ekrana durmadan 0 yazılır. Gövdesi tek deyim olsa bile döngülerde süslü parantez kullanmak bu hatayı baştan önler.
8.6.2double sayaç tuzağı
Aşağıdaki döngü x'i 0'dan başlatıp 0,1'er artırıyor. Amaç 10 tur dönmektir: x = 0,0; 0,1; …; 0,9.
int n = 0;
for (double x = 0; x < 1.0; x += 0.1) {
n++;
}
System.out.println(n);Program 10 değil, 11 yazar:
11Nedeni, 0,1 sayısının ikili tabanda tam gösterilememesidir. Onluk tabanda 1/3 = 0,333… sonsuza kadar sürer. Aynı biçimde ikili tabanda 1/10 = 0,0001100110011…2 sonsuza kadar sürer. double bu açılımı 53 bitte keser. Her toplamada küçük bir yuvarlama hatası oluşur ve bu hatalar birikir. Tablo 8.10 x'in her turdaki gerçek değerini Java'nın yazdığı biçimde verir.
| Tur | x (koşul sorulurken) | x < 1.0 | n (gövdeden sonra) |
|---|---|---|---|
| 1 | 0.0 | true | 1 |
| 2 | 0.1 | true | 2 |
| 3 | 0.2 | true | 3 |
| 4 | 0.30000000000000004 | true | 4 |
| 5 | 0.4 | true | 5 |
| 6 | 0.5 | true | 6 |
| 7 | 0.6 | true | 7 |
| 8 | 0.7 | true | 8 |
| 9 | 0.7999999999999999 | true | 9 |
| 10 | 0.8999999999999999 | true | 10 |
| 11 | 0.9999999999999999 | true | 11 |
| — | 1.0999999999999999 | false | 11 (döngü biter) |
4. turda ilk sapma görülür: x = 0,30000000000000004. 9. turda x = 0,7999999999999999 olur. 10 toplamadan sonra x tam 1,0 değil, 0,9999999999999999'dur. Bu değer 1,0'dan küçük olduğu için koşul doğru çıkar ve döngü bir tur fazla döner. x 1,0999999999999999 olunca döngü biter.
Koşul x != 1.0 yazılsaydı durum daha kötü olurdu. x hiçbir turda tam 1,0 olmadığı için koşul hep doğru kalır ve döngü bitmez. Denemede 1000 turdan sonra x yaklaşık 99,9999999999986'dır. Ders açıktır: döngü sayacı tam sayı olmalıdır. Gerçek sayı gerekiyorsa her turda sayaçtan hesaplanır: for (int k = 0; k < 10; k++) { double x = k * 0.1; … }. Bu döngü tam 10 tur döner.
Her ondalık adım hatalı değildir. 0,25 = 1/4 ikili tabanda tam gösterilir (0,012). for (double d = 0; d != 1.0; d += 0.25) döngüsü tam 4 tur döner ve biter. Ancak hangi sayının tam gösterilebildiğini her seferinde düşünmek hataya açıktır. Güvenli kural yine int sayaçtır.
8.6.3Döngü değişmezi
Bir döngünün doğru çalıştığını birkaç girdiyle denemek yararlıdır ama her girdiyi denemek mümkün değildir. Döngünün her girdide doğru sonuç verdiğini görmenin bir yolu, her turda değişmeden doğru kalan bir özellik bulmaktır.
Döngü değişmezi (loop invariant), döngünün her turunun başında doğru olan bir özelliktir. Döngüye girerken doğrudur; bir tur onu bozmaz; döngü bittiğinde de doğrudur ve o anda istenen sonucu verir.
1'den n'ye toplam döngüsünde değişmez şudur: her turun başında toplam = 1 + 2 + … + (i − 1). Yani toplam, o ana kadar işlenmiş sayıların toplamıdır. Tablo 8.11 n = 4 için bu özelliği her turun başında denetler.
| Turun başında i | toplam | 1 + … + (i − 1) | Değişmez |
|---|---|---|---|
| 1 | 0 | 0 (boş toplam) | doğru |
| 2 | 1 | 1 = 1 | doğru |
| 3 | 3 | 1 + 2 = 3 | doğru |
| 4 | 6 | 1 + 2 + 3 = 6 | doğru |
| 5 | 10 | 1 + 2 + 3 + 4 = 10 | doğru |
Değişmezin doğruluğu üç adımda gösterilir:
- Başlangıç. İlk turdan önce i = 1 ve toplam = 0'dır. 1 + … + 0 hiç terim içermeyen boş bir toplamdır ve değeri 0'dır. Değişmez baştan doğrudur.
- Koruma. Bir turun başında toplam = 1 + … + (i − 1) olsun. Gövde toplama i'yi ekler: toplam = 1 + … + (i − 1) + i. Güncelleme i'yi 1 artırır. Yeni i ile yazıldığında bu yine 1 + … + (i − 1) biçimindedir. Değişmez bir sonraki turun başında da doğrudur.
- Bitiş. Döngü i = n + 1 olduğunda biter. Değişmezde i yerine n + 1 yazılınca toplam = 1 + 2 + … + n çıkar. Bu tam olarak istenen sonuçtur.
Üç adımın hiçbiri n'nin belirli bir değerine bağlı değildir. Bu yüzden akıl yürütme her n için geçerlidir. Üs alma döngüsünde değişmez “her turun başında sonuc = tabani”, faktöriyel döngüsünde “faktoriyel, o ana kadar çarpılan sayıların çarpımıdır” biçimindedir. Bir döngü yazarken değişmezi tek cümleyle söyleyebilmek, döngünün ne yaptığını gerçekten bilmek demektir.
8.6.4Sınır hataları
En sık döngü hatası bir tur eksik ya da bir tur fazla dönmektir. Sayaç 0'dan mı 1'den mi başlamalı, koşul < mı <= mı olmalı? Bu küçük seçimlerdeki bir yanlış, döngünün ilk ya da son elemanı atlamasına veya fazladan bir eleman işlemesine yol açar.
Sınır hatası (off-by-one error), bir döngünün doğru sayıdan bir eksik ya da bir fazla tur dönmesiyle ortaya çıkan hatadır. Çoğunlukla başlangıç değerinin (0 ya da 1) veya karşılaştırma operatörünün (< ya da <=) yanlış seçilmesinden doğar.
Görev 1'den 5'e kadar saymak olsun. Şekil 8.11 ve Tablo 8.12 dört başlığın hangi değerleri işlediğini gösterir.
| Başlık | İşlenen i değerleri | Tur sayısı |
|---|---|---|
| for (i = 1; i <= 5; i++) | 1 2 3 4 5 | 5 |
| for (i = 1; i < 5; i++) | 1 2 3 4 | 4 |
| for (i = 0; i <= 5; i++) | 0 1 2 3 4 5 | 6 |
| for (i = 1; i <= 5; i--) | 1 0 −1 −2 … −2147483648 | 2147483650 |
Sonuncu başlık, sonsuz döngü kesimindeki taşmanın aynısıdır. i 1'den başlayıp −2 147 483 648'e kadar iner; bu 2 147 483 650 tur eder. Ardından değer sarar ve döngü biter.
Sık karşılaşılan bir başka kalıp 0'dan başlayan saymadır. Diziler ve dizgiler 0'dan numaralanır. Bu yüzden Java programlarında for (int i = 0; i < 5; i++) biçimi çok yaygındır. Bu döngü i = 0, 1, 2, 3, 4 için tam 5 tur döner:
int sayac = 0;
for (int i = 0; i < 5; i++) {
System.out.print(i + " ");
sayac++;
}
System.out.println("| tur: " + sayac);0 1 2 3 4 | tur: 5Koşul i <= 5 yazılırsa i = 5 de işlenir ve döngü 6 tur döner. Sayaç 5 yerine 6 olur:
0 1 2 3 4 5 | tur: 6Sınır hatasını önlemenin yolu basit bir denetimdir. Bir döngü yazıldıktan sonra üç soru sorulur: ilk turda sayaç hangi değeri alıyor, son turda hangi değeri alıyor ve toplam kaç tur dönüyor? for (int i = a; i < b; i++) biçiminde tur sayısı b − a, for (int i = a; i <= b; i++) biçiminde b − a + 1'dir. Bu iki formül çoğu sınır hatasını kod çalıştırılmadan yakalar.
8.7Rakam kalıbı
Döngülerle çözülen problemlerin çoğu birkaç ortak kalıba dayanır. Kalıp bir kez anlaşılınca aynı iskelet farklı problemlerde küçük değişikliklerle yeniden kullanılır. Bu kesimdeki örneklerin hepsi bir tam sayının rakamlarını tek tek işler. Basamak sayısı, rakamlar toplamı, ters çevirme, palindrom, Armstrong sayısı ve onluktan ikiliye çevirme aynı döngünün çeşitlemeleridir.
Kalıp (pattern), birçok problemde tekrar eden bir çözüm iskeletidir. Bir döngü kalıbı, döngünün türünü, ilk değerlerini, koşulunu ve güncellemesini belirler; problemden probleme yalnızca gövdedeki işlem değişir.
8.7.1% 10 ve / 10
Kalıbın temelinde iki tam sayı işlemi vardır. Pozitif bir tam sayı için sayi % 10, sayının 10'a bölümünden kalanı, yani son rakamı verir: 725 % 10 = 5. sayi / 10 ise tam sayı bölmesidir; kesirli kısım atılır ve son rakam silinir: 725 / 10 = 72. İki işlem art arda uygulandığında sayı sağdan sola doğru rakam rakam “tüketilir”:
725 → (5) → 72 → (2) → 7 → (7) → 0
Sayı 0 olduğunda işlenecek rakam kalmamıştır. Kalıbın iskeleti bu gözleme dayanır:
while (kalan > 0) {
int rakam = kalan % 10; // son rakamı al
// ... rakamla bir iş yap ...
kalan /= 10; // son rakamı sil
}Döngü, sayının basamak sayısı kadar tur döner. Altı basamaklı bir sayı için 6 tur yeter. Bu yüzden rakam kalıbı çok büyük sayılarda bile hızlıdır: en büyük int on basamaklıdır ve döngü en çok 10 tur döner.
İki ayrıntı her örnekte tekrarlanır. Birincisi, döngü sayının kendisini değil bir kopyasını (kalan) tüketir. Döngü bittiğinde kalan 0 olur ama özgün sayı korunur; palindrom ve Armstrong testinde sonuç onunla karşılaştırılır. İkincisi, negatif sayıların işareti önce atılır. Java'da −725 % 10 = −5'tir; kalan, bölünenin işaretini alır. İşaret atılmazsa while (kalan > 0) koşulu baştan yanlış çıkar ve döngü hiç çalışmaz.
8.7.2Basamak sayısı
int sayi = -507;
long kalan = sayi;
if (kalan < 0) kalan = -kalan;
int basamak = 0;
do {
basamak++;
kalan /= 10;
} while (kalan > 0);
System.out.println(basamak);| Tur | kalan (turun başında) | basamak | kalan / 10 | kalan > 0 |
|---|---|---|---|---|
| 1 | 507 | 1 | 50 | true |
| 2 | 50 | 2 | 5 | true |
| 3 | 5 | 3 | 0 | false |
Burada rakamın kendisi gerekmez; yalnızca kaç kez bölündüğü sayılır. Bu yüzden gövdede % 10 yoktur. basamak bir sayaçtır ve her turda 1 artar. Program 3 yazar.
Döngünün do-while olması bilinçli bir seçimdir. sayi = 0 için while döngüsü hiç çalışmaz ve 0 basamak bulur. Oysa 0 bir basamaklı bir sayıdır. do-while gövdeyi en az bir kez çalıştırdığı için 0 için de 1 bulur; ayrıca bir özel durum yazmak gerekmez. Aynı sonuç while döngüsüyle ve önüne eklenen if (sayi == 0) return 1; denetimiyle de alınabilir.
Kopyanın long olmasının da bir nedeni vardır. int'in en küçük değeri −2 147 483 648'dir ama en büyük değeri 2 147 483 647'dir. Bu yüzden −2 147 483 648'in pozitif karşılığı int'e sığmaz. Math.abs(-2147483648) bile yine −2 147 483 648 döndürür. Kopya long olunca işaret değiştirme güvenle yapılır ve uç değer için doğru sonuç (10) çıkar. Kopya int olsaydı bu girdi için yöntem 1 bulurdu.
8.7.3Rakamlar toplamı
Rakamlar toplamında her turda son rakam bir biriktiriciye eklenir:
int sayi = 725;
long kalan = sayi;
if (kalan < 0) kalan = -kalan;
int toplam = 0;
while (kalan > 0) {
toplam += (int) (kalan % 10);
kalan /= 10;
}
System.out.println(toplam);% 10 ile sırasıyla 5, 2, 7 alınır; toplam 5, 7, 14 olur.| Tur | kalan | kalan % 10 | toplam | kalan / 10 |
|---|---|---|---|---|
| 1 | 725 | 5 | 5 | 72 |
| 2 | 72 | 2 | 7 | 7 |
| 3 | 7 | 7 | 14 | 0 |
Program 14 yazar: 7 + 2 + 5 = 14. Burada while yeterlidir. sayi = 0 için gövde hiç çalışmaz ve toplam 0 kalır; bu sonuç doğrudur. (int) dönüşümü de gereklidir. kalan long olduğu için kalan % 10 da long'dur ve int bir değişkene eklenirken açıkça dönüştürülmelidir. Bileşik atama += bu dönüşümü kendisi de yapardı; açık yazım okuyana ne olduğunu gösterir. Aynı döngü −725 için de 14, 1001 için 2 verir.
8.7.4Ters çevirme ve palindrom
Bir sayıyı ters çevirmek için rakamlar sağdan sola alınır ve yeni bir sayı soldan sağa kurulur. Her turda eldeki ters sayı 10 ile çarpılır; böylece eski rakamlar bir basamak sola kayar ve birler basamağı boşalır. Boşalan yere yeni rakam eklenir.
int sayi = 1221;
int kalan = sayi, ters = 0;
while (kalan > 0) {
int rakam = kalan % 10;
ters = ters * 10 + rakam;
kalan /= 10;
}
System.out.println(ters);
System.out.println(ters == sayi); // palindrom mu?| Tur | kalan | rakam | ters = ters × 10 + rakam | kalan / 10 |
|---|---|---|---|---|
| 1 | 1221 | 1 | 0 × 10 + 1 = 1 | 122 |
| 2 | 122 | 2 | 1 × 10 + 2 = 12 | 12 |
| 3 | 12 | 2 | 12 × 10 + 2 = 122 | 1 |
| 4 | 1 | 1 | 122 × 10 + 1 = 1221 | 0 |
| ters == sayi | true |
Rakamları tersten okunduğunda da aynı kalan sayıya palindrom (palindrome) denir. 1221, 7 ve 12321 palindromdur; 123 değildir.
Program önce 1221'i, sonra true yazar. Palindrom testi, ters sayının özgün sayıya eşit olup olmadığına bakar. Bu yüzden döngü özgün sayıyı değil, kopyasını tüketir. 123 için tablo şöyle olur:
| Tur | kalan | rakam | ters = ters × 10 + rakam | kalan / 10 |
|---|---|---|---|---|
| 1 | 123 | 3 | 0 × 10 + 3 = 3 | 12 |
| 2 | 12 | 2 | 3 × 10 + 2 = 32 | 1 |
| 3 | 1 | 1 | 32 × 10 + 1 = 321 | 0 |
| ters == sayi | false |
Sondaki sıfırları unutmak. 120'nin tersi bu yöntemle 21 çıkar, çünkü 021 diye bir int yoktur. 1200 de 21'e döner. Palindrom testi bu sayılar için yine doğru sonuç verir (120 ≠ 21). Ancak tersi ekranda “021” biçiminde göstermek gerekiyorsa sayı değil, karakter dizisi kurulmalıdır.
8.7.5Armstrong sayısı
Rakamlarının basamak sayısı kadar kuvvetleri toplamı kendisine eşit olan sayıya Armstrong sayısı (Armstrong number) denir. Üç basamaklı bir sayı için kuvvet 3'tür: 153 = 13 + 53 + 33 = 1 + 125 + 27.
int sayi = 153;
int kalan = sayi, toplam = 0;
while (kalan > 0) {
int r = kalan % 10;
toplam += r * r * r; // 3 basamak: küp
kalan /= 10;
}
System.out.println(toplam == sayi);| Tur | kalan | r | r × r × r | toplam |
|---|---|---|---|---|
| 1 | 153 | 3 | 27 | 27 |
| 2 | 15 | 5 | 125 | 152 |
| 3 | 1 | 1 | 1 | 153 |
Kalıp rakamlar toplamının aynısıdır; yalnızca rakam yerine rakamın küpü eklenir. Toplam 153'e ulaşır ve program true yazar. Bu kod yalnızca üç basamaklı sayılar için doğrudur, çünkü kuvvet sabit 3 alınmıştır. Genel bir çözüm önce basamak sayısını bulur, sonra her rakamın o kuvvetini alır. 100'den 999'a kadar bütün sayılar denendiğinde dört Armstrong sayısı çıkar: 153, 370, 371 ve 407.
8.7.6Onluktan ikiliye çevirme
Kalıbın en ilginç çeşitlemesi taban değiştirmedir. 10 yerine 2 kullanılınca n % 2 sayının ikili tabandaki son bitini, n / 2 ise o biti silinmiş sayıyı verir. Bitler sağdan sola elde edilir. Aşağıdaki program ikili gösterimi ondalık rakamlarla (1 ve 0) bir int'te kurar. Her yeni bit bir öncekinin bir basamak soluna yazılır; bunun için basamak değişkeni 1, 10, 100, … diye büyür.
int n = 13;
int ikili = 0, basamak = 1;
while (n > 0) {
ikili += (n % 2) * basamak;
basamak *= 10;
n /= 2;
}
System.out.println(ikili); // 1101| Tur | n | n % 2 | basamak | ikili | n / 2 |
|---|---|---|---|---|---|
| 1 | 13 | 1 | 1 | 1 | 6 |
| 2 | 6 | 0 | 10 | 1 | 3 |
| 3 | 3 | 1 | 100 | 101 | 1 |
| 4 | 1 | 1 | 1000 | 1101 | 0 |
Program 1101 yazar. Denetim: 1 · 8 + 1 · 4 + 0 · 2 + 1 · 1 = 13. Bu yöntemin bir sınırı vardır. İkili gösterim ondalık rakamlarla kurulduğu için sonuç çabuk büyür. 1023'ün ikili gösterimi on tane 1'dir (1111111111) ve int'e sığar. 1024'ün ikili gösterimi 10000000000, yani on bir basamaklıdır ve int'e sığmaz. Program bu girdi için sessizce taşar ve 1410065408 yazar. Büyük sayılar için sonuç karakter dizisi olarak kurulur ya da Java'nın Integer.toBinaryString metodu kullanılır.
8.8Çarpım ve bölen kalıpları
8.8.1Üs alma
Bir tabanın tam sayı kuvveti, tabanın kendisiyle tekrar tekrar çarpılmasıdır: 24 = 2 × 2 × 2 × 2. Tekrar sayısı (üs) bellidir; bu yüzden for döngüsü uygundur.
int taban = 2, us = 4;
int sonuc = 1;
for (int i = 0; i < us; i++) {
sonuc *= taban;
}
System.out.println(sonuc);| Tur | i | i < us | sonuc |
|---|---|---|---|
| 1 | 0 | true | 2 |
| 2 | 1 | true | 4 |
| 3 | 2 | true | 8 |
| 4 | 3 | true | 16 |
| — | 4 | false | 16 (döngü biter) |
Döngü tam us kez çalışır ve her turda bir kez tabanla çarpar. Değişmez “her turun başında sonuc = 2i” biçimindedir. i = 4'te döngü biter ve sonuc = 24 = 16 olur. Çarpım biriktiricisi 1'den başlar. us = 0 ise döngü hiç çalışmaz ve sonuç 1 kalır; bu da 20 = 1 ile uyuşur.
Kod negatif üsleri kapsamaz; us negatifse döngü hiç dönmez ve yanlış olarak 1 bulunur. Büyük sonuçlar da int sınırını aşar. 230 = 1 073 741 824 sığar. 231 int'te −2 147 483 648, 232 ise 0 çıkar. Taşma bir hata iletisi üretmez; sonuç sessizce yanlış olur.
8.8.2Mükemmel sayı
Bölen kalıbında döngü, sayının olası bölenlerini tek tek dener. i, n'yi bölüyorsa n % i == 0 olur.
Kendisi hariç pozitif bölenlerinin toplamı kendisine eşit olan sayıya mükemmel sayı (perfect number) denir. En küçük örnek 6 = 1 + 2 + 3'tür.
int n = 28;
int toplam = 0;
for (int i = 1; i <= n / 2; i++) {
if (n % i == 0) toplam += i;
}
System.out.println(toplam == n);n'nin kendisi hariç en büyük böleni en fazla n / 2'dir. n / 2'den büyük ve n'den küçük bir sayı n'yi bölemez. Bu yüzden döngü n / 2'de durur ve gereksiz turlardan kaçınır. Tablo 8.20 28 için 14 turu gösterir.
| i | 28 % i | Bölen mi? | toplam |
|---|---|---|---|
| 1 | 0 | evet | 1 |
| 2 | 0 | evet | 3 |
| 3 | 1 | hayır | 3 |
| 4 | 0 | evet | 7 |
| 5 | 3 | hayır | 7 |
| 6 | 4 | hayır | 7 |
| 7 | 0 | evet | 14 |
| 8 | 4 | hayır | 14 |
| 9 | 1 | hayır | 14 |
| 10 | 8 | hayır | 14 |
| 11 | 6 | hayır | 14 |
| 12 | 4 | hayır | 14 |
| 13 | 2 | hayır | 14 |
| 14 | 0 | evet | 28 |
Bölenler 1, 2, 4, 7 ve 14'tür. Toplamları 1 + 2 + 4 + 7 + 14 = 28 olduğu için program true yazar. 10 000'e kadar denendiğinde yalnızca dört mükemmel sayı çıkar: 6, 28, 496 ve 8128.
8.8.3Asal sayı testi
1'den büyük olup 1 ve kendisinden başka pozitif böleni olmayan tam sayıya asal sayı (prime number) denir. 0 ve 1 asal değildir; 2 tek çift asaldır.
int n = 29;
boolean asal = n >= 2;
for (int i = 2; asal && i <= n / i; i++) {
if (n % i == 0) asal = false;
}
System.out.println(asal);Kodun her satırında bir fikir vardır. asal = n >= 2 ataması 0, 1 ve negatif sayıları baştan eler. Döngü 2'den başlayarak bölen arar. Bir bölen bulununca asal = false olur ve koşuldaki asal && döngüyü durdurur. Burada break yerine bir bayrak kullanılmıştır. Bayrak, durumu bir mantıksal değişkende tutar ve döngünün çıkış koşulunu başlıkta görünür kılar.
İkinci fikir aramanın sınırıdır. n'nin 1 ve kendisinden başka bir böleni varsa, bu bölenlerden en az biri √n'den büyük değildir. Çünkü n = a × b ise a ve b'nin ikisi birden √n'den büyük olamaz; olsaydı çarpımları n'yi geçerdi. Bu yüzden i'yi √n'ye kadar denemek yeter. Koşuldaki i <= n / i, i × i ≤ n ile aynı anlamdadır ama i * i çok büyük i için taşabilir; bölme taşmaz.
| n | i | n / i | asal && i <= n / i | n % i | asal |
|---|---|---|---|---|---|
| 29 | 2 | 14 | true | 1 | true |
| 29 | 3 | 9 | true | 2 | true |
| 29 | 4 | 7 | true | 1 | true |
| 29 | 5 | 5 | true | 4 | true |
| 29 | 6 | 4 | false | — | true |
| 21 | 2 | 10 | true | 1 | true |
| 21 | 3 | 7 | true | 0 | false |
| 21 | 4 | hesaplanmaz | false | — | false |
29 için 4 bölen denenir ve hiçbiri 29'u bölmez. i = 6'da 6 <= 29 / 6 yani 6 <= 4 yanlıştır. √29 ≈ 5,39 geçilmiştir ve daha büyük bölen aramaya gerek yoktur. Program true yazar.
21 için i = 3'te 21 % 3 = 0 olur ve asal = false yapılır. Güncelleme i'yi 4'e çıkarır. Koşulun ilk parçası asal artık yanlıştır. && operatörü kısa devre yapar: ilk işlenen yanlışsa ikincisi hiç hesaplanmaz. i <= n / i denenmeden döngü biter ve program false yazar. Yalnızca 2 bölen denenmiştir.
√n sınırının kazancı büyüktür. 97 için yalnızca 8 aday (2…9) denenir. En büyük int olan 2 147 483 647 asaldır; bu sayı için 46 339 deneme yeter. 2'den n − 1'e kadar arayan bir döngü aynı iş için iki milyardan fazla tur dönerdi.
8.8.41'den n'ye asal sayılar
Asal testi bir dış döngünün içine konunca 2'den n'ye kadar bütün asallar listelenir. Her k için iç döngü asal testini yapar:
int n = 20;
for (int k = 2; k <= n; k++) {
boolean asal = true;
for (int i = 2; i <= k / i; i++) {
if (k % i == 0) { asal = false; break; }
}
if (asal) System.out.print(k + " ");
}Burada iç döngü break ile biter. asal değişkeni her k için yeniden true yapılır; bu atama dış döngünün içinde olmalıdır. Döngünün dışına taşınırsa ilk bileşik sayıdan sonra bayrak hep false kalır ve 2, 3'ten sonra hiçbir sayı yazılmaz. Tablo 8.22 her k için denenen bölenleri verir.
| k | Denenen i değerleri | Sonuç |
|---|---|---|
| 2 | yok (2 > 2 / 2) | asal, yazılır |
| 3 | yok (2 > 3 / 2) | asal, yazılır |
| 4 | 2 (4 % 2 = 0, break) | asal değil |
| 5 | 2 | asal, yazılır |
| 6 | 2 (6 % 2 = 0, break) | asal değil |
| 7 | 2 | asal, yazılır |
| 8 | 2 (8 % 2 = 0, break) | asal değil |
| 9 | 2, 3 (9 % 3 = 0, break) | asal değil |
| 10 | 2 (10 % 2 = 0, break) | asal değil |
| 11 | 2, 3 | asal, yazılır |
| 12 | 2 (12 % 2 = 0, break) | asal değil |
| 13 | 2, 3 | asal, yazılır |
| 14 | 2 (14 % 2 = 0, break) | asal değil |
| 15 | 2, 3 (15 % 3 = 0, break) | asal değil |
| 16 | 2 (16 % 2 = 0, break) | asal değil |
| 17 | 2, 3, 4 | asal, yazılır |
| 18 | 2 (18 % 2 = 0, break) | asal değil |
| 19 | 2, 3, 4 | asal, yazılır |
| 20 | 2 (20 % 2 = 0, break) | asal değil |
2 ve 3 için iç döngü hiç çalışmaz, çünkü 2 <= k / 2 yanlıştır (2 / 2 = 1 ve 3 / 2 = 1). Çift sayıların hepsi ilk denemede, i = 2'de elenir. 9 ve 15 ikinci denemede, i = 3'te elenir. 17 ve 19 için 2, 3 ve 4 denenir; hiçbiri bölmez. Program şunu yazar:
2 3 5 7 11 13 17 19 2..20 arasında 8 asal vardır; 2..100 arasında 25. Çok sayıda asal gerektiğinde her sayıyı ayrı ayrı test etmek yerine Eratosthenes eleği gibi daha hızlı yöntemler kullanılır. Bu yöntem bir asal bulduğunda onun bütün katlarını tek geçişte işaretler.
8.8.5Fibonacci dizisi
Fibonacci dizisi (Fibonacci sequence) 0 ve 1 ile başlayan, sonraki her terimi kendisinden önceki iki terimin toplamı olan dizidir: 0, 1, 1, 2, 3, 5, 8, 13, …
Diziyi üretmek için yalnızca son iki terimi tutmak yeter. a sıradaki terimi, b ondan sonrakini tutar. Her turda a yazılır, sonraki terim hesaplanır ve iki değişken bir adım “kaydırılır”:
int adet = 5;
long a = 0, b = 1;
for (int i = 0; i < adet; i++) {
System.out.print(a + " ");
long sonraki = a + b;
a = b;
b = sonraki;
}| i | Yazılan a | sonraki = a + b | Yeni a | Yeni b |
|---|---|---|---|---|
| 0 | 0 | 1 | 1 | 1 |
| 1 | 1 | 2 | 1 | 2 |
| 2 | 1 | 3 | 2 | 3 |
| 3 | 2 | 5 | 3 | 5 |
| 4 | 3 | 8 | 5 | 8 |
Çıktı 0 1 1 2 3'tür. Kaydırmanın sırası önemlidir: önce a = b, sonra b = sonraki. Sıra ters yazılırsa (önce b = sonraki, sonra a = b) a, b'nin eski değerini değil yeni değerini alır. İki değişken hep eşit olur ve her terim bir öncekinin iki katı çıkar: 0 1 2 4 8. sonraki değişkeni tam da bu nedenle vardır; eski değeri kaybetmeden yeni değeri tutar.
Dizi üstel hızla büyür. Bu yüzden değişkenler long seçilmiştir. long bile sonsuz değildir. 93. terim 7 540 113 804 746 346 429'dur ve long'a sığar. 94. terim long'un en büyük değerini (9 223 372 036 854 775 807) aşar; program onu negatif bir sayı olarak yazar.
8.9Girdiyle çalışan döngüler
Şimdiye kadarki döngülerin tur sayısı programın içindeki sayılara bağlıydı. Kullanıcıdan girdi okuyan döngülerde ise döngünün ne zaman biteceğine kullanıcı karar verir. Bu döngülerde tur sayısı baştan bilinmez ve doğal seçim while ya da do-while olur.
8.9.1Sayı tahmin oyunu
Bilgisayar 1 ile 100 arasında gizli bir sayı tutar. Kullanıcı tahmin eder; program “Yukarı” ya da “Aşağı” diyerek yol gösterir. Doğru tahmin yapılınca kaçıncı denemede bilindiği yazılır.
Random rnd = new Random();
Scanner klavye = new Scanner(System.in);
int hedef = rnd.nextInt(100) + 1; // 1..100
int tahmin = 0, deneme = 0;
while (tahmin != hedef) {
tahmin = klavye.nextInt();
deneme++;
if (tahmin < hedef) System.out.println("Yukarı");
if (tahmin > hedef) System.out.println("Aşağı");
}
System.out.println(deneme + ". denemede bildin");Rastgele sayı üretmek için java.util.Random sınıfı kullanılır; dosyanın başına Random ve Scanner için iki import satırı gerekir. rnd.nextInt(100) 0 ile 99 arasında bir sayı verir; 1 eklenince aralık 1..100 olur. Her çalıştırmada farklı bir hedef çıkar.
Döngüden önce tahmin = 0 verilmesinin nedeni koşuldur. Hedef 1..100 arasında olduğu için 0 hiçbir zaman hedef olamaz. Böylece tahmin != hedef ilk denetimde kesinlikle doğru çıkar ve döngüye girilir. Aynı iş do-while ile de yazılabilirdi; o durumda bu yapay başlangıç değerine gerek kalmazdı.
Hedefin 37 çıktığı bir çalıştırmada kullanıcı sırayla 50, 25 ve 37 girsin. Tablo 8.24 oyunun üç turunu gösterir. Tablo, rastgele sayı üreticisine 37'yi verecek bir başlangıç tohumu verilerek JDK 21 ile alınmıştır.
| Tur | tahmin | deneme | İleti | Olası aralık |
|---|---|---|---|---|
| 1 | 50 | 1 | Aşağı | 1...49 |
| 2 | 25 | 2 | Yukarı | 26...49 |
| 3 | 37 | 3 | yok | bulundu |
İlk tahmin 50'dir. 50 > 37 olduğu için “Aşağı” yazılır; hedef artık 1..49 aralığındadır. İkinci tahmin 25'tir; “Yukarı” yazılır ve aralık 26..49'a iner. Üçüncü tahmin 37'dir. İki if de yanlıştır ve bir ileti yazılmaz. Koşul 37 != 37 yanlış çıkar ve döngü biter. Program “3. denemede bildin” yazar.
Her seferinde aralığın ortasını söylemek, aralığı yaklaşık yarıya indirir. 100 sayılık bir aralık 7 yarılamada tek sayıya iner; bu yüzden bu stratejiyle her hedef en çok 7 denemede bulunur. Bu düşünce ikili aramanın temelidir.
8.9.2Bekçi değerle okuma
Kullanıcı kaç sayı gireceğini önceden söylemezse verinin bittiği nasıl anlaşılır? Yaygın çözüm, veri olamayacak özel bir değeri “bitti” işareti olarak kullanmaktır.
Bekçi değer (sentinel value), bir veri dizisinin sonunu işaretleyen ve kendisi veriye katılmayan özel değerdir. Bekçi değer, gerçek verinin hiçbir zaman alamayacağı bir değer olmalıdır.
Scanner sc = new Scanner(System.in);
int toplam = 0;
while (true) {
int sayi = sc.nextInt();
if (sayi == 0) break;
toplam += sayi;
}
System.out.println("Toplam: " + toplam);Döngü bilerek sonsuz yazılmıştır: while (true). Çıkış gövdenin ortasındaki break ile sağlanır. Sayı okunur okunmaz bekçi değer olup olmadığına bakılır; bekçi değilse toplama eklenir. Tablo 8.25 girdi 5, 3, 0 için turları gösterir.
| Okunan sayi | sayi == 0 | toplam |
|---|---|---|
| 5 | false | 5 |
| 3 | false | 8 |
| 0 | true | 8 (break) |
Program “Toplam: 8” yazar. 0 toplama eklenmemiştir. Kullanıcı ilk sayı olarak 0 girerse döngü ilk turda biter ve “Toplam: 0” yazılır. Girdi 7, −2, 4, 0 için toplam 9'dur; negatif sayılar bu programda geçerli veridir, çünkü bekçi yalnızca 0'dır.
Bekçiyi işlemden sonra denetlemek ya da veri olabilecek bir değeri bekçi seçmek. toplam += sayi; satırı if'ten önce yazılırsa bekçi de toplanır. Bekçi 0 iken bu fark etmez ama bekçi −1 olsaydı toplam 1 eksik çıkardı. Not ortalaması gibi bir programda ise 0 gerçek bir not olabilir. Bekçi değer 0 seçilirse 0 alan öğrencinin notu “veri bitti” diye yorumlanır. Böyle durumlarda −1 gibi veri aralığının dışında bir bekçi seçilir.
Bekçi değerli okumanın tipik bir uzantısı ortalama hesabıdır. Toplamın yanında bir sayaç tutulur ve döngüden sonra ortalama toplam / adet olarak hesaplanır. İki ayrıntı unutulmamalıdır: hiç sayı girilmemişse (adet = 0) bölme yapılmaz ve bölmeden önce (double) dönüşümü yapılır. 80, 64, 90, 0 girdisi için toplam 234, ortalama 78,0 çıkar; 6, 9, 0 için ortalama 7,5'tir. Dönüşüm yapılmasaydı tam sayı bölmesi 15 / 2 = 7 verirdi.
8.10İç içe döngüler
8.10.1Satır ve sütun
Bir döngünün gövdesinde başka bir döngü bulunabilir. Bu yapıya iç içe döngü (nested loop) denir. Dıştaki döngüye dış döngü, içtekine iç döngü denir. Tablolar, ızgaralar, çarpım tabloları ve ekran desenleri satır-sütun düzenindedir; bu tür çıktıların doğal aracı iç içe döngüdür.
for (int satir = 0; satir < 3; satir++) {
for (int sutun = 0; sutun < 4; sutun++) {
System.out.printf("(%d,%d) ", satir, sutun);
}
System.out.println();
}(0,0) (0,1) (0,2) (0,3)
(1,0) (1,1) (1,2) (1,3)
(2,0) (2,1) (2,2) (2,3) İç içe döngünün temel kuralı şudur: iç döngü, dış döngünün her turunda baştan sona tamamlanır. satir = 0 iken iç döngü sutun = 0, 1, 2, 3 için dört kez çalışır ve (0,0)'dan (0,3)'e kadar dört hücre yazar. sutun 4 olunca iç döngü biter. Ardından println() satırı sonlandırır. Dış döngü satir++ ile 1'e geçer ve iç döngü sutun = 0'dan yeniden başlar. sutun değişkeni iç döngünün başlığında tanımlandığı için her dış turda yeniden oluşturulur ve 0 değerini alır.
İç gövde toplam satır sayısı × sütun sayısı = 3 × 4 = 12 kez çalışır. printf içindeki %d yer tutucuları, virgülden sonra verilen değerlerle sırayla doldurulur. println() ise dış döngünün gövdesinde ama iç döngünün dışında yer alır. Bu konum her satırın sonunda bir kez alt satıra geçilmesini sağlar.
İç döngünün sınırı dış döngünün değişkenine bağlı olabilir. O durumda iç gövdenin çalışma sayısı bir çarpım değil, bir toplam olur. Örneğin i 1'den 4'e, j de i'den 4'e kadar dönerse iç gövde 4 + 3 + 2 + 1 = 10 kez çalışır. Artan üçgen deseni bu yapının bir örneğidir.
8.10.2Çarpım tablosu
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
System.out.printf("%4d", i * j);
}
System.out.println();
} 1 2 3
2 4 6
3 6 9%4d her sayıyı 4 karakterlik bir alana sağa yaslar.Dış döngü i = 1 iken iç döngü j = 1, 2, 3 için 1, 2, 3 yazar. j = 4'te iç koşul yanlış olur ve println satırı bitirir. Aynı iş i = 2 ve i = 3 için tekrarlanır. %4d biçimi sayıları sütunlar hâlinde hizalar: tek basamaklı sayıların önüne üç, iki basamaklı sayıların önüne iki boşluk eklenir. Sınırlar 10 yapıldığında 10 × 10'luk klasik çarpım tablosu çıkar; en büyük sayı 100 olduğu için 4 karakterlik alan yine yeter.
8.11Desenler
Yıldızlardan ya da sayılardan oluşan ekran desenleri iç içe döngü alıştırmasının klasik konusudur. Desenlerin kendisi önemli değildir; önemli olan, bir çıktının düzenini sayılarla ifade edip o sayıları döngü sınırlarına çevirmektir. Bu beceri tablolar, raporlar ve matrislerle çalışırken de gerekir.
Desen (pattern), satır satır yazılan ve her satırdaki boşluk ve karakter sayısı satır numarasına göre düzenli değişen çıktıdır. Dış döngü satırları, iç döngüler ise bir satırdaki boşluk ve karakter parçalarını üretir.
8.11.1Desen problemini çözme yöntemi
Bir desen dört adımda koda dönüştürülür:
- Çiz. Beklenen çıktı küçük bir n için (örneğin n = 4) kareli kâğıda çizilir. Her kare bir karakterdir; boşluklar da kare sayılır.
- Say. Her satırdaki boşluk ve karakter sayıları bir tabloya yazılır.
- İlişkilendir. Sayılar satır numarasıyla ilişkilendirilir. Örneğin 3, 2, 1, 0 dizisi n − satır, 1, 2, 3, 4 dizisi satır, 1, 3, 5, 7 dizisi 2 × satır − 1'dir.
- Kodla. Dış döngü satır sayısı kadar döner. Satırdaki her parça (boşluklar, yıldızlar) için bir iç döngü yazılır; iç döngünün sınırı 3. adımda bulunan formüldür. Satır sonunda println çağrılır.
Tablo 8.26 dört temel desenin formüllerini toplar. Sağdaki sayılar, desenler JDK 21 ile n = 4 için gerçekten üretilip her satırdaki boşluk ve yıldızlar sayılarak elde edilmiştir.
| Desen | Boşluk | Karakter |
|---|---|---|
| Artan üçgen | 0 | satır |
| Azalan üçgen | 0 | n − satır + 1 |
| Sağa yaslı | n − satır | satır |
| Piramit | n − satır | 2 × satır − 1 |
| Satır | Artan | Azalan | Sağa yaslı | Piramit |
|---|---|---|---|---|
| 1 | 0 + 1 | 0 + 4 | 3 + 1 | 3 + 1 |
| 2 | 0 + 2 | 0 + 3 | 2 + 2 | 2 + 3 |
| 3 | 0 + 3 | 0 + 2 | 1 + 3 | 1 + 5 |
| 4 | 0 + 4 | 0 + 1 | 0 + 4 | 0 + 7 |
8.11.2Artan üçgen ve sayı üçgeni
Artan üçgende satır r'de r yıldız vardır ve boşluk yoktur. Tek bir iç döngü yeter; üst sınırı dış döngünün değişkenidir:
int n = 4;
for (int satir = 1; satir <= n; satir++) {
for (int sutun = 1; sutun <= satir; sutun++) {
System.out.print("*");
}
System.out.println();
}*
**
***
****satir = 1 iken iç döngü yalnızca sutun = 1 için çalışır ve bir yıldız yazar. sutun = 2'de 2 <= 1 yanlıştır ve println alt satıra geçer. satir = 2 iken iç döngü iki kez, satir = 3 iken üç kez, satir = 4 iken dört kez döner. satir = 5'te dış koşul yanlış olur. Toplam 1 + 2 + 3 + 4 = 10 yıldız yazılır. Genel olarak n satırlık artan üçgende n(n + 1) / 2 yıldız vardır; n = 4 için 4 · 5 / 2 = 10.
Aynı iskelet karakter yerine sayı da yazabilir. Sayı üçgeninde her hücreye iç döngünün değişkeni yazılır:
int n = 4;
for (int satir = 1; satir <= n; satir++) {
for (int deger = 1; deger <= satir; deger++) {
System.out.print(deger + " ");
}
System.out.println();
}1
1 2
1 2 3
1 2 3 4 Dış döngü yine satırı, iç döngünün üst sınırı satırdaki eleman sayısını belirler. Fark yalnızca print çağrısının içindedir: "*" yerine deger + " " yazılır. Her sayıdan sonra bir boşluk bırakıldığı için satırların sonunda da bir boşluk vardır; ekranda görünmez. İç döngünün değişkeni yerine dış döngünün değişkeni yazılsaydı her satırda aynı sayı tekrarlanırdı: 1, 2 2, 3 3 3, 4 4 4 4.
8.11.3Boşluklu desenler: sağa yaslı üçgen ve piramit
Sağa yaslı üçgende her satır iki parçadan oluşur: önce boşluklar, sonra yıldızlar. Boşluk sayısı azalırken yıldız sayısı artar. Her parça için ayrı bir iç döngü yazılır:
int n = 3;
for (int satir = 1; satir <= n; satir++) {
for (int bosluk = 0; bosluk < n - satir; bosluk++)
System.out.print(" ");
for (int yildiz = 0; yildiz < satir; yildiz++)
System.out.print("*");
System.out.println();
} *
**
***n = 3 için satir = 1'de boşluk döngüsü n − satir = 2 kez döner, yıldız döngüsü 1 kez. satir = 2'de 1 boşluk ve 2 yıldız yazılır. satir = 3'te boşluk döngüsünün koşulu 0 < 3 − 3 baştan yanlıştır; döngü hiç çalışmaz ve satır doğrudan 3 yıldızla başlar. Her satır tam n = 3 karakterdir. Bu denetim, boşluk ve yıldız formüllerinin birbiriyle tutarlı olduğunu gösterir: (n − satır) + satır = n.
Piramit aynı iskeletin bir çeşitlemesidir. Boşluk sayısı yine n − satır, yıldız sayısı ise 2 × satır − 1'dir. Yıldızlar her satırda ikişer artar; boşluklar birer azalır ve yıldız grubu ortada kalır. Aşağıdaki program n = 4 için önce piramidi, sonra karşılaştırma için azalan üçgeni çizer:
public class Piramit {
public static void main(String[] args) {
int n = 4;
for (int satir = 1; satir <= n; satir++) {
for (int b = 0; b < n - satir; b++) System.out.print(" ");
for (int y = 0; y < 2 * satir - 1; y++) System.out.print("*");
System.out.println();
}
for (int satir = 1; satir <= n; satir++) {
for (int y = 1; y <= n - satir + 1; y++) System.out.print("*");
System.out.println();
}
}
} *
***
*****
*******
****
***
**
*Çıktının ilk dört satırı piramittir. Son dört satır aynı programın devamındaki azalan üçgendir: yıldız sayısı n − satır + 1, yani 4, 3, 2, 1'dir. Piramitteki toplam yıldız sayısı 1 + 3 + 5 + 7 = 16 = 42'dir. İlk n tek sayının toplamı her zaman n2'dir.
8.11.4İçi boş kare
Bazı desenlerde bir hücreye ne yazılacağı yalnızca satıra değil, hem satıra hem sütuna bağlıdır. İçi boş karede kenardaki hücreler yıldız, içteki hücreler boşluktur. İç döngünün gövdesine bir if-else girer:
int n = 4;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 || i == n - 1 || j == 0 || j == n - 1)
System.out.print("*");
else
System.out.print(" ");
}
System.out.println();
}****
* *
* *
****Bir hücre dört kenardan birindeyse yıldızdır: ilk satır (i == 0), son satır (i == n - 1), ilk sütun (j == 0) ya da son sütun (j == n - 1). Dört koşul || ile bağlanmıştır; biri doğruysa yeter. Kısa devre nedeniyle ilk doğru koşuldan sonrakiler hesaplanmaz. Tablo 8.28 her hücrede hangi koşulun karar verdiğini gösterir.
| i | j = 0 | j = 1 | j = 2 | j = 3 |
|---|---|---|---|---|
| 0 | * (i == 0) | * (i == 0) | * (i == 0) | * (i == 0) |
| 1 | * (j == 0) | boşluk | boşluk | * (j == n - 1) |
| 2 | * (j == 0) | boşluk | boşluk | * (j == n - 1) |
| 3 | * (i == n - 1) | * (i == n - 1) | * (i == n - 1) | * (i == n - 1) |
16 hücrenin 12'si kenar, 4'ü iç hücredir. else dalındaki boşluk yazımı zorunludur. Bu satır atlanırsa iç hücrelere hiçbir şey yazılmaz. Sağ kenardaki yıldız sola kayar ve ortadaki satırlar ** olarak görünür; kare bozulur.
8.11.5Desenlerde sık hatalar
println çağrısını iç döngünün içine koymak. Bu durumda her yıldızdan sonra satır biter. n = 3 için artan üçgen yerine alt alta 1 + 2 + 3 = 6 satır, her satırda tek yıldız çıkar. println iç döngünün kapanış parantezinden sonra, dış döngünün içine yazılmalıdır.
Desenlerdeki diğer tipik yanlışlar şunlardır. < ile <= karıştırılırsa her satırda bir eksik ya da bir fazla karakter çıkar; sayaç 0'dan başlıyorsa <, 1'den başlıyorsa <= kullanılır. Boşluk döngüsü yıldız döngüsünden sonra yazılırsa boşluklar satırın sonuna düşer, görünmez ve desen sola yaslanır. Dış döngünün değişkeni iç döngüde yanlışlıkla değiştirilirse (örneğin iç başlıkta satir++ yazılırsa) satırlar atlanır ya da döngü bitmez. Her desen, küçük bir n için izleme tablosuyla satır satır denetlenerek bu hatalardan korunur.
8.12Test tasarımı
Bir döngünün tek bir girdiyle doğru çalışması, her girdide doğru çalışacağını göstermez. Döngü hataları genellikle sınırlarda ortaya çıkar: hiç dönmemesi gereken, tek tur dönen ya da en büyük değeri işleyen durumlarda. İyi bir test kümesi bu durumların her birinden bir örnek içerir.
Test durumu (test case), bir program için seçilen girdi ile o girdide beklenen çıktının ikilisidir. Sınır durumu (edge case), girdi aralığının ucundaki ya da özel davranış gerektiren değerdir: 0, 1, negatif sayılar ve tipin en büyük ya da en küçük değeri gibi.
Bir test kümesi şu adımlarla hazırlanır:
- Her problem için sıradan, “normal” bir değer seçilir.
- 0, 1, negatif değerler ve üst sınıra yakın değerler ayrıca seçilir.
- Her girdi için beklenen sonuç, kod çalıştırılmadan elle hesaplanır.
- Program çalıştırılır. Sonuç beklenenden farklıysa değişkenler tur tur izlenir ve ilk sapmanın olduğu tur bulunur.
Basamak sayısı programı için dört test yeterli bir başlangıçtır. Tablo 8.29 bu testleri ve JDK 21 ile alınan sonuçları verir.
| Basamak sayısı testi | sayı | beklenen | JDK 21 sonucu |
|---|---|---|---|
| Normal | 4096 | 4 | 4 |
| Sıfır | 0 | 1 | 1 |
| Negatif | −507 | 3 | 3 |
| Uç değer | −2147483648 | 10 | 10 |
Her test belirli bir hatayı yakalamak için seçilmiştir. 0 testi, döngü while ile yazılmışsa 0 sonucunu verir ve hatayı gösterir. Negatif test, işaret atılmamışsa döngünün hiç çalışmadığını ortaya çıkarır. Uç değer testi, kopya int olarak tutulmuşsa yanlış sonuç (1) verir. Normal test ise kodun temel mantığını doğrular. Dört testin dördünden de geçen bir çözüm, bu hataların hiçbirini içermez.
Aynı düşünce her döngüye uygulanır. Bir toplam döngüsü n = 0 (hiç tur), n = 1 (tek tur) ve büyük bir n ile denenir. Bir asal testi 0, 1, 2, 4, bir kare sayı (örneğin 25) ve büyük bir asal ile denenir. Bir desen programı n = 1 ve n = 0 ile de çalıştırılır. Döngünün sıfır kez, bir kez ve çok kez döndüğü durumların her biri en az bir testle kapsanmalıdır.
8.13Özet
- Her döngü dört parçadan oluşur: ilk değer verme, koşul, gövde ve güncelleme. Güncelleme koşulu sona yaklaştırır; unutulursa döngü bitmez.
fordöngüsünde üç parça başlıkta toplanır ve sıra “ilk değer verme → koşul → gövde → güncelleme → koşul …” biçimindedir. 1'den n'ye dönen bir for döngüsünde gövde n, koşul n + 1 kez çalışır.whilekoşulu gövdeden önce,do-whilegövdeden sonra sorar. while gövdesi hiç çalışmayabilir; do-while gövdesi en az bir kez çalışır.- Tekrar sayısı belliyse for, koşul sürdükçe while, en az bir tur gerekiyorsa do-while seçilir. Üçü birbirine çevrilebilir; seçim okunabilirlik içindir.
breaken yakın döngüyü bitirir,continueyalnızca o turun kalanını atlar. Etiketli break iç içe döngülerden birden çıkar.- Sonsuz döngü yanlış yönde ya da hiç yapılmayan güncellemeden doğar. double sayaç yuvarlama hatası biriktirir; döngü sayacı int olmalıdır.
- Döngü değişmezi her turun başında doğru kalan bir özelliktir; döngü bittiğinde sonucun doğruluğunu gösterir. Sınır hatalarına karşı ilk tur, son tur ve tur sayısı denetlenir.
- Rakam kalıbı (
% 10son rakamı verir,/ 10onu siler) basamak sayısı, rakamlar toplamı, ters çevirme, palindrom, Armstrong sayısı ve taban değiştirmede kullanılır. - Bölen kalıbı
n % i == 0denetimine dayanır. Mükemmel sayıda n / 2'ye, asal testinde √n'ye kadar aramak yeter. - Bekçi değer veri olamayacak bir değerdir; veri sonunu işaretler ve kendisi işlenmez.
- İç içe döngüde iç döngü dış döngünün her turunda baştan sona tamamlanır. Desenlerde dış döngü satırı, iç döngüler satırdaki boşluk ve karakterleri üretir; sayılar satır numarasına bağlanır.
- Döngüler normal, 0, 1, negatif ve uç değerlerle test edilir; beklenen sonuç önceden elle hesaplanır.
8.14Kavram listesi
- döngü loop
- döngü gövdesi loop body
- tur iteration
- ilk değer verme initialization
- koşul condition
- döngü değişkeni loop variable
- kapsam scope
- sayaç counter
- biriktirici accumulator
- artırma / azaltma increment / decrement
- ön koşullu döngü pre-test loop
- son koşullu döngü post-test loop
- menü menu
- girdi doğrulama input validation
- break break statement
- etiketli break labeled break
- sonsuz döngü infinite loop
- taşma overflow
- kayan noktalı sayı floating-point number
- döngü değişmezi loop invariant
- sınır hatası off-by-one error
- kalıp pattern
- rakam digit
- tam sayı bölmesi integer division
- palindrom palindrome
- Armstrong sayısı Armstrong number
- üs / taban exponent / base
- mükemmel sayı perfect number
- bölen divisor
- asal sayı prime number
- bayrak flag
- kısa devre değerlendirme short-circuit evaluation
- Fibonacci dizisi Fibonacci sequence
- rastgele sayı random number
- bekçi değer sentinel value
- iç içe döngü nested loop
- iç döngü / dış döngü inner loop / outer loop
- desen pattern (output)
- kod izleme code tracing
- izleme tablosu trace table
- test durumu test case
- sınır durumu edge case
8.15Kendini Dene
for (int i = 2; i <= 20; i *= 3) System.out.print(i + " ");döngüsü ne yazar? Gövde ve koşul kaçar kez çalışır?Cevap
i sırasıyla 2, 6, 18 değerlerini alır ve
2 6 18yazılır. 18'den sonra i = 18 × 3 = 54 olur ve 54 <= 20 yanlıştır. Gövde 3 kez, koşul 4 kez çalışır. Güncelleme her zaman 1 artırma olmak zorunda değildir; burada sayaç çarpılarak ilerler.int x = 1; while (x < 100) x *= 2;döngüsünden sonra x kaçtır? Döngü kaç tur döner?Cevap
x sırasıyla 2, 4, 8, 16, 32, 64, 128 olur. 64 < 100 doğru olduğu için bir tur daha döner ve x = 128 olur; 128 < 100 yanlıştır. Döngü 7 tur döner ve x = 128'dir. Döngü, koşulu yanlış yapan ilk değerde biter; bu değer sınırı aşmış olabilir.
int k = 10; do { k -= 4; } while (k > 0);çalıştıktan sonra k kaçtır? Gövde kaç kez çalışır? Aynı döngüwhile (k > 0) k -= 4;ve başlangıç değeri k = −1 ile yazılsaydı ne olurdu?Cevap
k sırasıyla 6, 2, −2 olur. Her turun sonunda koşul sorulur; 6 ve 2 için doğru, −2 için yanlıştır. Gövde 3 kez çalışır, k = −2'dir. k = −1 ile başlayan while döngüsünde −1 > 0 baştan yanlıştır; gövde hiç çalışmaz ve k = −1 kalır. Aynı başlangıçla do-while bir kez çalışır ve k = −5 olurdu.
Aşağıdaki döngü ne yazar?
for (int i = 1; i <= 10; i++) { if (i % 2 == 0) continue; if (i > 7) break; System.out.print(i + " "); }Cevap
Çift sayılar continue ile atlanır. Tek sayılardan 1, 3, 5 ve 7 yazılır. i = 9'da 9 tektir, ilk koşul yanlıştır, 9 > 7 doğrudur ve break döngüyü bitirir. Çıktı
1 3 5 7'dir. i = 8 continue ile atlandığı için break'e 8'de değil 9'da sıra gelir.Rakam kalıbıyla 4096'nın rakamlar toplamı ve tersi nedir? 1200'ün tersi ve palindrom testi ne verir? 0 için ters sayı ve palindrom testi ne verir?
Cevap
4096: rakamlar sağdan sola 6, 9, 0, 4'tür. Toplam 6 + 9 + 0 + 4 = 19, ters 6904'tür; 6904 ≠ 4096 olduğundan palindrom değildir. 1200: rakamlar 0, 0, 2, 1'dir ve ters sayı 0 → 0 → 2 → 21 diye kurulur; sonuç 21'dir, palindrom değildir. 0 için while döngüsü hiç çalışmaz; ters = 0 kalır ve 0 == 0 olduğundan test
trueverir. 0 tek rakamlı bir sayı olarak gerçekten palindromdur.Aşağıdaki iç içe döngüden sonra sayac kaçtır?
for (int i = 1; i <= 4; i++) for (int j = i + 1; j <= 4; j++) sayac++;(sayac başta 0)Cevap
i = 1 için j = 2, 3, 4 (3 tur); i = 2 için j = 3, 4 (2 tur); i = 3 için j = 4 (1 tur); i = 4 için iç döngü hiç dönmez (5 <= 4 yanlış). Toplam 3 + 2 + 1 + 0 = 6'dır. Bu sayı, 4 elemandan seçilebilecek farklı ikililerin sayısıdır: 4 · 3 / 2 = 6.
n = 4 için aşağıdaki desen isteniyor. Satır r'deki boşluk ve yıldız sayıları nedir? İç döngülerin sınırları ne olmalıdır?
**** *** ** *Cevap
Satır 1'de 0 boşluk ve 4 yıldız, satır 2'de 1 boşluk ve 3 yıldız, satır 3'te 2 boşluk ve 2 yıldız, satır 4'te 3 boşluk ve 1 yıldız vardır. Boşluk sayısı r − 1, yıldız sayısı n − r + 1'dir. Her satır (r − 1) + (n − r + 1) = n karakterdir. Kod:
for (int b = 0; b < satir - 1; b++)boşluklar için,for (int y = 0; y < n - satir + 1; y++)yıldızlar için.for (double d = 0; d != 1.0; d += 0.25)döngüsü biter mi? Kaç tur döner? Aynı döngüd += 0.1ile yazılsaydı ne olurdu?Cevap
0,25 = 1/4 ikili tabanda tam gösterilir (0,012). d sırasıyla 0, 0,25, 0,5, 0,75 ve tam 1,0 olur. Döngü 4 tur döner ve biter. 0,1 ise ikili tabanda tam gösterilemez; 10 toplamadan sonra d = 0,9999999999999999 olur ve hiçbir turda tam 1,0'a eşit olmaz.
!=koşuluyla bu döngü bitmez. Güvenli yol int sayaçtır.Asal testi (
asal && i <= n / ikoşuluyla) n = 97 için çalıştırılıyor. Kaç bölen adayı denenir? Döngü hangi i değerinde ve neden biter?Cevap
i = 2, 3, …, 9 denenir; hiçbiri 97'yi bölmez. Bu 8 adaydır. i = 9'da 9 <= 97 / 9 = 10 doğrudur. i = 10'da 10 <= 97 / 10 = 9 yanlıştır ve döngü biter. √97 ≈ 9,85 olduğundan daha büyük bölen aramaya gerek yoktur. Program
trueyazar.
8.16İleri okuma
- R. Sedgewick, K. Wayne, Computer Science: An Interdisciplinary Approach (Addison-Wesley, 2016), Kesim 1.3: koşullar ve döngüler Java'da, izleme tablolarıyla ve kuvvetler, harmonik sayılar, Newton yöntemi ve ikili gösterim gibi kısa programlarla anlatılır; kesimin çevrim içi sürümü introcs.cs.princeton.edu adresindedir.
- A. B. Downey, C. Mayfield, Think Java (2. baskı, O'Reilly, 2019): while ve for döngüleri ile iç içe döngüler programlamaya yeni başlayanlar için sade bir dille anlatılır; kitabın çevrim içi sürümü ücretsizdir.
- Oracle, The Java Tutorials: while ve do-while, for ve dallanma deyimleri sayfaları; etiketli break ve continue'nun resmî örnekleri bu sayfalardadır.
8.17Kaynaklar
- P. Deitel, H. Deitel, Java: How to Program, Early Objects, 11. baskı, Pearson, 2018.
- Y. D. Liang, Introduction to Java Programming and Data Structures, 13. baskı, Pearson, 2024.
- Oracle, The Java Tutorials: while and do-while, for, Branching Statements.
- R. Sedgewick, K. Wayne, Computer Science: An Interdisciplinary Approach, Addison-Wesley, 2016: 1.3 Conditionals and Loops.
- A. B. Downey, C. Mayfield, Think Java, 2. baskı, O'Reilly, 2019.
İlgili sayfalar: Döngüler sunumu · Çalışma soruları · Döngü İzleyici simülatörü · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.