Java ile Nesne Yönelimli Programlama · Ders Notu
Bölüm 3.3: Diziler
Bir sınıfın sınav notları, bir sensörün saniyede verdiği ölçümler ya da bir resmin pikselleri aynı türden pek çok değerden oluşur. Bu değerlerin her biri için ayrı bir değişken açmak hem yorucudur hem de değer sayısı değişince kodu bozar. Dizi bu sorunu çözer: aynı tipten çok sayıda değeri tek bir ad altında, numaralı hücrelerde tutar. Not önce dizi kavramını, indisi ve dizinin bellekteki yerleşimini anlatır. Ardından Java'da dizi tanımlamayı, ilk değer vermeyi, length'i, for-each döngüsünü ve java.util.Arrays sınıfını işler. Dizi değişkeninin bir referans olması, kopyalama ve metoda geçirme bu kısmın en önemli konusudur. Sonraki kesimler sınır dışı hatasını, istatistik hesaplarını (ortalama, ortanca, mod), arama ve sıralama algoritmalarını, maliyetlerini ve çok boyutlu dizileri ele alır. Not, iki boyutlu bir karakter dizisi üzerine kurulan Tic Tac Toe oyunuyla biter.
Öğrenme çıktıları
- Kavram
- Dizinin aynı tipten, sabit sayıda öğeyi ardışık hücrelerde tuttuğunu ve öğelere 0'dan başlayan indisle erişildiğini açıklamak.
- Söz dizimi
- Tek ve çok boyutlu dizileri tanımlamak;
newve ilklendirici listesiyle oluşturmak;lengthile dolaşmak. - Bellek
- Dizi değişkeninin bir referans olduğunu, dizinin heap'te durduğunu göstermek; atama, kopyalama ve metoda geçirmenin sonucunu öngörmek.
- Hatalar
ArrayIndexOutOfBoundsException'ı ve sık derleme hatalarını tanımak; sınır denetimiyle önlemek.- Algoritmalar
- Ortalama, ortanca, mod, doğrusal ve ikili arama, eşleştirme, tersine çevirme, seçimli ve kabarcık sıralamasını yazmak; O(1), O(log n), O(n) ve O(n²) maliyetlerini karşılaştırmak.
- Uygulama
- Matrisleri iç içe döngülerle doldurmak, yazdırmak, transpozunu almak ve
char[][]ile Tic Tac Toe oyunu kurmak.
3.3.1Dizi kavramı
Şimdiye kadarki programlarda her değişken tek bir değer tuttu: bir sayı, bir karakter ya da bir dizgi. Gerçek problemlerde ise değerler çoğu zaman topluluk hâlinde gelir. Elli öğrencinin notu, bir gün boyunca dakikada bir ölçülen sıcaklık ya da bir devredeki düğüm gerilimleri böyle topluluklardır. Bu kesim diziyi tanımlar, neden gerektiğini gösterir ve dizinin bellekte nasıl durduğunu anlatır.
3.3.1.1Dizi nedir?
Dizi (array), aynı tipten, sabit sayıda değeri tek bir ad altında, ardışık hücrelerde tutan yapıdır. Dizideki her değere öğe (element), öğenin dizideki konum numarasına indis (index) denir. İlk öğenin indisi 0'dır; n öğeli bir dizinin son indisi n − 1'dir.
Dizi, aynı adla tanımlanan ve birden çok değer tutabilen bir değişken gibi düşünülebilir. Tek bir değişkenin bir adı ve bir değeri vardır. Dizinin ise tek bir adı, ama çok sayıda değeri vardır. Değerler birbirinden indisle ayrılır. Bir öğeye erişmek için dizinin adı yazılır, ardından köşeli parantez içinde indis gelir: dizi[0], dizi[1], dizi[2].
İki gündelik benzetme kavramı somutlaştırır. Birincisi bir apartmandır. Apartmanın tek bir adresi vardır; bu adres dizinin adına karşılık gelir. Daireler ise numaralıdır ve “3 numara” demek doğru daireyi bulmaya yeter. Kimse “sağdan ikinci, üst kattaki kapı” gibi bir tarif vermez. İkincisi bir yumurta kolisidir. Kolide kaç hücre olduğu baştan bellidir ve koli sonradan büyümez. Her hücreye yalnız aynı türden bir şey konur. Dizinin iki temel özelliği, sabit boyut ve tek tip, bu benzetmede görünür.
İndisin 0'dan başlaması yeni başlayanlara garip gelir. Gündelik hayatta sayma 1'den başlar: birinci öğrenci, ikinci öğrenci. Java, C ve onlardan türeyen diller ise indisi bir uzaklık olarak görür. İndis, öğenin dizinin başından kaç hücre ötede olduğunu söyler. İlk öğe başlangıçtan 0 hücre uzaktadır, dolayısıyla indisi 0'dır. Bu bakış, öğenin bellekteki adresinin hesabını da kolaylaştırır: i. öğe, başlangıç adresinden i hücre ötededir.
dizi = [10, 20, 30, 40, 50] olsun. Dizide 5 öğe vardır, dolayısıyla geçerli indisler 0, 1, 2, 3 ve 4'tür.
dizi[0] ilk öğedir ve değeri 10'dur. dizi[4] son öğedir ve değeri 50'dir. Üçüncü öğe dizi[2]'dir; indis, sıra numarasının bir eksiğidir. dizi[5] diye bir öğe yoktur. Böyle bir erişim Java'da çalışma zamanında hata verir.
3.3.1.2Neden dizi?
Elli öğrencinin notlarının ortalaması hesaplanacak olsun. Dizi kullanmadan her not için ayrı bir değişken gerekir:
int not1 = 70;
int not2 = 85;
int not3 = 90;
// ... not50'ye kadar
int toplam = not1 + not2 + not3 /* + ... */;Bu yaklaşımın üç sorunu vardır. Birincisi, toplam ifadesine elli adın tek tek yazılması gerekir. İkincisi, sınıfa bir öğrenci katılırsa programın değişmesi gerekir: yeni bir değişken açılır ve toplama eklenir. Üçüncüsü, değişkenlerin adları yalnız programcı için anlamlıdır. not1 ile not2 arasındaki “sıradalık” ilişkisini Java bilmez. Bir döngü not ve i'yi birleştirerek not3 adını üretemez.
Dizi bu üç sorunu birlikte çözer:
int[] notlar = new int[50];
// ... notlar doldurulur
int toplam = 0;
for (int i = 0; i < notlar.length; i++) {
toplam += notlar[i];
}Burada notlar tek bir addır ve elli hücreyi birden temsil eder. Döngü i'yi 0'dan 49'a kadar sayar ve her turda notlar[i] ile bir hücreye bakar. Aynı kod 5 not için de 5000 not için de çalışır; yalnız dizinin boyutu değişir. Döngü koşulundaki notlar.length dizinin kaç hücresi olduğunu söylediği için koşulun elle değiştirilmesi de gerekmez.
notlar[i] yazıldığında hangi hücreye bakılacağını i'nin o anki değeri belirler; şekilde i = 2 iken 90 değeri okunur.Dizinin asıl gücü Şekil 3.3.1'de görülür. İndis sabit bir sayı olmak zorunda değildir; bir değişken ya da bir ifade olabilir. notlar[i] yazıldığında hangi hücreye bakılacağı derleme sırasında değil, çalışma zamanında i'nin değerine göre belirlenir. Bu yüzden tek bir satır kod, döngü içinde bütün hücrelere sırayla uygulanabilir.
3.3.1.3Bellekte bir dizi
Dizinin öğeleri bellekte rastgele yerlere dağılmaz. Hücreler yan yana, ardışık olarak yerleşir. Şekil 3.3.2 beş öğeli bir int dizisini gösterir. Her hücrenin üstünde indisi, içinde değeri vardır.
a dizisi. Hücreler bellekte yan yanadır; indisler 0'dan 4'e gider. a değişkeni hücrelerin kendisini değil, dizinin başlangıcını gösteren bir referansı tutar.| indis | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| öğe | 13 | −1 | 4 | 2 | 7 |
Tablo 3.3.1'e göre a[1] = −1, a[4] = 7 ve a.length = 5'tir. a[5] diye bir hücre yoktur; geçerli indisler 0…4'tür. Bu ayrım, dizilerle ilgili hataların çoğunun kaynağıdır. Dizinin uzunluğu hücre sayısıdır; son indis ise uzunluğun bir eksiğidir.
Şekildeki ok da önemlidir. a adlı değişkenin içinde beş sayı yoktur. Değişken, beş sayının durduğu bellek bölgesini gösteren bir referans tutar. Bu ayrıntı ilk bakışta önemsiz görünür, ama dizi atamasının, kopyalamanın ve metoda geçirmenin nasıl çalıştığını belirler.
3.3.1.4Avantajlar ve dezavantajlar
Ardışık yerleşim, dizinin hem en büyük üstünlüğünün hem de en büyük kısıtının kaynağıdır.
Üstünlükler şunlardır. Dizi verileri düzenli ve ardışık biçimde saklar. İndisle erişim hızlıdır: dizi[i] her i için aynı sürede okunur. İlk öğeye erişmek ile milyonuncu öğeye erişmek arasında fark yoktur. Veriler gruplandığı için döngülerle kolayca işlenir. Bellek kullanımı da yalındır: dizi yalnız öğeleri ve uzunluğunu tutar. Bağlı liste gibi yapılarda her öğenin yanında bir de sonraki öğenin adresi saklanır; dizide bu ek yük yoktur.
Kısıtlar da aynı yerleşimden doğar. Birincisi, dizi sabit boyutludur: oluşturulduktan sonra boyutu değiştirilemez. Yer kalmadığında yeni bir öğe eklemek için daha büyük yeni bir dizi oluşturulur ve eski öğeler oraya kopyalanır. İkincisi, ortaya ekleme ya da ortadan silme öğelerin kaydırılmasını gerektirir. Üçüncüsü, dizi yalnız tek tipten öğe tutar.
[10, 15, 20, 30, 40] olur. (b) Aynı kaydırma oklarla: her öğe sağındaki hücreye taşınır.Şekil 3.3.3 ekleme işleminin maliyetini gösterir. 15 değeri indis 1'e konacaksa indis 1'den sonraki bütün öğeler bir hücre sağa taşınmalıdır. Kaydırmanın yönü önemlidir. Soldan sağa kaydırılırsa önce 20, 30'un üzerine yazılır ve 30 kaybolur. Bu yüzden sondaki öğeden başlanır:
int[] d = {10, 20, 30, 40, 0}; dizisinde son hücre boştur. 15 değeri indis 1'e eklensin.
for (int i = 3; i >= 1; i--) d[i + 1] = d[i]; döngüsü önce d[4] = d[3] (40), sonra d[3] = d[2] (30), en son d[2] = d[1] (20) atamalarını yapar. Ardından d[1] = 15; yazılır.
Sonuç [10, 15, 20, 30, 40] olur. Eklenen konumdan sonra k öğe varsa k kaydırma gerekir. En kötü durumda, başa eklemede, bütün öğeler kayar; bu yüzden ekleme O(n) iş ister.
Boyutu çalışma sırasında değişen listeler için Java'da ArrayList sınıfı vardır. ArrayList içeride yine bir dizi kullanır. Dizi dolunca daha büyük bir dizi oluşturur ve öğeleri oraya kopyalar. Programcı bu işi görmez, ama maliyet ortadan kalkmaz; yalnız gizlenir. ArrayList ve öteki koleksiyonlar java.util paketindedir.
3.3.1.5Dizi türleri ve kullanım alanları
Diziler boyut sayısına göre üç gruba ayrılır. Tek boyutlu dizide öğeler tek bir satırda durur; int[] notlar böyledir. İki boyutlu dizide öğeler satır ve sütunlara yerleşir; bu bir matristir ve int[][] tablo biçiminde yazılır. Çok boyutlu dizi daha fazla köşeli parantezle yazılır; int[][][] kup üç boyutludur.
Dizi, programlama dillerinde en yaygın veri yapısıdır. Elektrik-elektronik mühendisliğinde sık karşılaşılan üç kullanım şunlardır:
- Ölçümler. Bir sensörden saniyede alınan örnekler
double[]dizisinde tutulur. Sayısallaştırılmış bir ses sinyali de böyledir. 44,1 kHz ile örneklenen bir saniyelik ses, 44 100 öğeli bir dizidir. - Matrisler. Devre analizindeki düğüm denklemleri, doğrusal denklem sistemleri ve dönüşüm matrisleri iki boyutlu dizilerle ifade edilir.
- Resimler. Gri tonlu bir resim,
int[yukseklik][genislik]biçiminde bir piksel ızgarasıdır. Renkli resim bir boyut daha ister: her piksel için kırmızı, yeşil ve mavi kanalları.
Bir sıcaklık sensörü 1000 ölçüm veriyor ve ölçümler double[] olcum = new double[1000]; dizisinde tutuluyor. Öğeler kaç bayt yer kaplar?
Java'da her double 8 bayttır. 1000 · 8 = 8000 bayt, yani yaklaşık 8 KB gerekir. Aynı ölçümler int olsaydı her biri 4 bayt tutar, toplam 4000 bayt olurdu. JVM dizinin uzunluğu ve tipi için birkaç bayt daha ayırır; bu ek, öğelerin yanında küçüktür.
Bu hesap tasarımda işe yarar. Bir milyon öğeli double dizisi 8 MB, 1000 × 1000'lik bir double matrisi de yine 8 MB tutar. Boyut sayısı arttıkça bellek hızla büyür.
3.3.2Java'da diziler
Dizi kavramı bütün dillerde benzerdir, ama ayrıntılar dilden dile değişir. Java'da dizinin nasıl tanımlandığı, hücrelerin hangi değerle başladığı, uzunluğun nereden okunduğu ve dizi değişkeninin ne tuttuğu bu kesimin konusudur. Kesim, dizilerle sık kullanılan java.util.Arrays sınıfıyla biter.
3.3.2.1Dizi tanımlama ve varsayılan değerler
Java'da bir dizi şu genel kalıpla tanımlanır ve oluşturulur:
VeriTipi[] diziAdi = new VeriTipi[diziBoyutu];Kalıbın üç parçası vardır. VeriTipi, dizide saklanacak değerlerin tipidir: int, double, char, String ya da herhangi bir sınıf. diziAdi, dizinin adıdır ve değişken adlandırma kurallarına uyar. diziBoyutu dizinin kaç öğe içereceğini söyler. Boyut bir sabit olabileceği gibi, çalışma zamanında hesaplanan bir int de olabilir.
Satırda aslında iki ayrı iş yapılır. Sol taraf, VeriTipi[] diziAdi, bir dizi değişkeni tanımlar. Sağ taraf, new VeriTipi[diziBoyutu], belleğin heap adı verilen bölgesinde diziye yer ayırır. Eşittir işareti de bu yeni dizinin referansını değişkene yazar. Değişken ve dizi ayrı şeylerdir. Bu ayrım ilerideki bütün referans konularının temelidir.
int[] sayilar = new int[5]; // 5 tamsayı
double[] olcum = new double[100]; // 100 ondalık
String[] isimler = new String[30];
int n = 12;
char[] harfler = new char[n]; // boyut değişkenden
int dizi2[] = new int[3]; // C tarzı, geçerli ama önerilmezBu satırların hepsi derlenir. sayilar beş, olcum yüz, isimler otuz öğelidir. harfler dizisinin boyutu bir değişkenden gelir: n 12 olduğu için dizi 12 öğelidir. Son satırdaki int dizi2[] yazımı C dilinden kalmadır. Java bunu kabul eder, ama köşeli parantezin tipe bitişik yazılması önerilir: int[] “int dizisi” diye okunur ve tipin tamamı tek yerde görünür.
Boyut, oluşturma anında belli olmalıdır. Sonradan değişmez. new int[n] yazıldıktan sonra n'nin değeri değişse de dizinin boyutu aynı kalır. Boyut negatif olursa dizi oluşturulamaz; program NegativeArraySizeException fırlatır:
Exception in thread "main" java.lang.NegativeArraySizeException: -2
at NegBoyut.main(NegBoyut.java:4)Boyutu 0 olan bir dizi ise geçerlidir. new int[0] hiç hücresi olmayan bir dizidir ve length'i 0'dır. Böyle bir dizi, “hiç sonuç yok” durumunu null kullanmadan anlatmak için işe yarar.
Varsayılan değerler
new ile oluşturulan dizinin hücreleri boş kalmaz. Java her hücreye tipine göre bir varsayılan değer (default value) yazar.
| Tip | Varsayılan değer |
|---|---|
int, long, short, byte | 0 |
double, float | 0.0 |
boolean | false |
char | '\u0000' (kodu 0 olan karakter) |
String ve bütün nesne tipleri | null |
Varsayılan değerler yerel değişkenlerle karşılaştırıldığında ilginçtir. int x; diye tanımlanan bir yerel değişken değer almadan okunursa derleyici variable x might not have been initialized hatası verir. Dizi hücreleri ise hiçbir zaman “değersiz” değildir. new int[5] beş tane 0 içerir ve hemen okunabilir.
char dizisinin varsayılan değeri ayrıca dikkat ister. '\u0000' boşluk karakteri değildir; kodu 0 olan, ekranda görünmeyen bir karakterdir. Boşluk karakterinin kodu 32'dir. Boş hücreleri boşlukla göstermek isteyen bir program, char dizisini önce ' ' ile doldurmalıdır. String[] dizisinin hücreleri de boş dizgi ("") değil, null'dır. null hiçbir nesneyi göstermeyen referanstır ve üzerinde bir metot çağrılırsa NullPointerException oluşur.
String[] isimler = new String[30]; satırından sonra isimler[0].length() çağrılır. Hücre null olduğu için program NullPointerException ile durur. new String[30] otuz dizgi oluşturmaz; otuz dizgiyi gösterebilecek otuz boş referans oluşturur. Her hücreye ayrıca bir dizgi atanmalıdır.
3.3.2.2Erişim ve length
Dizi öğelerine indisle erişilir ve indis 0'dan başlar. dizi[i] ifadesi hem okumak hem yazmak için kullanılır. Eşittir işaretinin sağında okur, solunda yazar. Dizinin boyutu length özelliğinden alınır.
int[] sayilar = {10, 20, 30, 40, 50};
int ilkEleman = sayilar[0]; // İlk elemanı alır
int ikinciEleman = sayilar[1]; // İkinci elemanı alır
int diziBoyutu = sayilar.length; // 5
System.out.println(ilkEleman + " " + ikinciEleman
+ " " + diziBoyutu);
sayilar[1] = 25; // yazma
int son = sayilar[sayilar.length - 1]; // 50Program şu satırı yazar:
10 20 5sayilar[0] ilk öğeyi (10), sayilar[1] ikinci öğeyi (20) okur. sayilar.length 5'tir. println'daki " " dizgileri sayıları birbirinden ayırır; aralarında dizgi olmasaydı ilkEleman + ikinciEleman toplama yapar ve 30 yazardı. sayilar[1] = 25; satırı ikinci hücreye yazar. Son satır ise son öğeyi okumanın genel kalıbıdır: sayilar[sayilar.length - 1]. Dizi kaç öğeli olursa olsun, bu ifade son öğeyi verir.
sayilar dizisi yazma işleminden sonra. sayilar[1] = 25 ikinci hücreyi değiştirmiştir. length = 5 hücre sayısıdır; son hücre [length − 1], yani [4]'tür.length bir metot değil, dizinin bir özelliğidir (alanıdır). Bu yüzden parantezsiz yazılır: sayilar.length. String sınıfındaki length() ise bir metottur ve parantezle yazılır. Bu iki yazım kolayca karışır:
E_LengthParen.java:4: error: cannot find symbol
System.out.println(a.length());
^
symbol: method length()
location: variable a of type int[]
1 errorİleti, int[] tipindeki a değişkeninde length() adlı bir metot bulunamadığını söyler. Tersi de hatalıdır: bir String değişkeni için s.length yazılırsa derleyici variable length bulamaz. Kısa bir kural akılda tutulabilir: dizide parantez yok, dizgide parantez var.
Dizinin length alanı final'dır, yani değiştirilemez. sayilar.length = 10; satırı derlenmez. Dizinin boyutunun sabit olması bu kuralla da güvenceye alınmıştır.
3.3.2.3İlk değer verme
Dizinin öğeleri baştan biliniyorsa her hücreyi tek tek atamaya gerek yoktur. Değerler süslü parantez içinde, virgülle ayrılarak verilir. Bu yazıma ilklendirici listesi (array initializer) denir.
int[] sayilar = {10, 20, 30, 40, 50}; // 5 elemanlı
int[] n = {1, 2, 3, 4, 5}; // 5 değer olduğundan n 5 elemanlı bir dizi olur
int[] m = new int[]{1, 2, 3}; // uzun biçim: her yerde kullanılabilir
String[] gunler = {"Pzt", "Sal", "Çar"};
char[] sesli = {'a', 'e', 'ı', 'i', 'o', 'ö', 'u', 'ü'};İlklendirici listesi kullanıldığında boyut yazılmaz. Boyutu listedeki değer sayısı belirler: n dizisinde beş değer vardır, bu yüzden n.length 5'tir. gunler üç, sesli sekiz öğelidir. Java karakterleri Unicode olarak tuttuğu için 'ı', 'ö' ve 'ü' gibi Türkçe harfler de geçerli char değerleridir.
İlklendirici listesinin iki biçimi vardır. Kısa biçim, {1, 2, 3}, yalnız tanımlama satırında kullanılabilir. Uzun biçim, new int[]{1, 2, 3}, her yerde geçerlidir: atamada, metot çağrısında ve return'de. Dizi önce tanımlanıp sonra kısa biçimle doldurulmak istenirse derleyici hata verir:
E_KisaSonra.java:4: error: illegal start of expression
a = {1, 2, 3};
^
E_KisaSonra.java:4: error: not a statement
a = {1, 2, 3};
^
E_KisaSonra.java:4: error: ';' expected
a = {1, 2, 3};
^
3 errorsİletideki üç hatadan asıl olanı ilkidir: illegal start of expression. Derleyici = işaretinden sonra bir ifade bekler ve süslü parantezi bir ifadenin başı olarak tanımaz. Sonraki iki ileti bu ilk hatanın yol açtığı karışıklıktır. Derleyici bir hatadan sonra çoğu zaman bir dizi ek ileti üretir; ilk ileti düzeltilince ötekiler de kaybolur. Doğru yazım a = new int[]{1, 2, 3};'tür.
Boyut ile listeyi birlikte vermek de yasaktır. Liste zaten boyutu belirlediği için Java iki bilginin çelişme olasılığına izin vermez:
E_IkisiBirden.java:3: error: array creation with both dimension expression and initialization is illegal
int[] a = new int[3]{1, 2, 3};
^
1 errorHiçbirini vermemek de hatadır. new int[] yazılıp ardından liste gelmezse derleyici dizinin kaç hücreli olacağını bilemez:
E_BoyutYok.java:3: error: array dimension missing
int[] a = new int[];
^
1 errorListedeki değerler dizinin tipine uymalıdır. int dizisine 2.5 konamaz, çünkü double'dan int'e dönüşüm kesirli kısmı atar ve Java bunu kendiliğinden yapmaz:
E_Lossy.java:3: error: incompatible types: possible lossy conversion from double to int
int[] a = {1, 2.5, 3};
^
1 errorTers yön sorun çıkarmaz. double[] d = {1, 2, 3}; geçerlidir; int değerler kayıpsız biçimde double'a genişler ve dizi 1.0, 2.0, 3.0 tutar. double[] c = {-40, 0, 25, 100}; listesi de bu yüzden derlenir.
C dilinden gelen alışkanlıkla int a[3] = {1, 2, 3}; yazılır. Java'da boyut değişkenin tipinde yer almaz; javac ']' expected iletisini verir. Java'da tip yalnız “int dizisi”dir; kaç hücre olduğu nesnenin özelliğidir, değişkenin değil. Doğrusu int[] a = {1, 2, 3};'tür.
3.3.2.4Kod izleme: DiziOrnegi
Aşağıdaki program bir diziyi tanımlar, hücrelerine tek tek değer atar, iki öğeyi okur ve son olarak bütün öğeleri bir döngüyle yazdırır. Kısa olmasına karşın dizinin bütün temel işlemlerini içerir.
public class DiziOrnegi {
public static void main(String[] args) {
// Bir diziyi tanımlama: 5 elemanlı bir tamsayı dizisi
int[] sayilar = new int[5];
// Dizinin elemanlarına değer atama
sayilar[0] = 10;
sayilar[1] = 20;
sayilar[2] = 30;
sayilar[3] = 40;
sayilar[4] = 50;
// Dizinin elemanlarına erişme
System.out.println("birinci eleman: " + sayilar[0]);
System.out.println("ucuncu eleman: " + sayilar[2]);
// Dizinin tüm elemanlarını döngü içerisinde yazdırma
System.out.print("Elemanlar: ");
for (int i = 0; i < sayilar.length; i++) {
System.out.print(sayilar[i] + " ");
}
}
}Programın çıktısı:
birinci eleman: 10
ucuncu eleman: 30
Elemanlar: 10 20 30 40 50 new int[5] heap'te beş hücrelik bir dizi oluşturur. Her hücre varsayılan değer 0 ile başlar. sayilar değişkeni bu dizinin referansını tutar. Ardından beş atama satırı hücreleri sırayla doldurur. Birinci öğenin indisi 0'dır, bu yüzden sayilar[0] 10'u okur. Üçüncü öğe sayilar[2]'dir; indis, sıra numarasının bir eksiğidir.
Son kısımda print kullanılır. print, println'dan farklı olarak alt satıra geçmez; bu yüzden bütün öğeler aynı satırda, aralarında birer boşlukla yazılır. Satırın sonunda da bir boşluk kalır. Döngü i'yi 0'dan başlatır ve i < sayilar.length koşulu doğru oldukça devam eder.
i | i < sayilar.length | Okunan hücre | Ekrana eklenen |
|---|---|---|---|
| 0 | 0 < 5 doğru | sayilar[0] = 10 | 10 |
| 1 | 1 < 5 doğru | sayilar[1] = 20 | 20 |
| 2 | 2 < 5 doğru | sayilar[2] = 30 | 30 |
| 3 | 3 < 5 doğru | sayilar[3] = 40 | 40 |
| 4 | 4 < 5 doğru | sayilar[4] = 50 | 50 |
| 5 | 5 < 5 yanlış | — | döngü biter |
new int[5] sonrası: beş hücre de 0'dır. (b) Üç atamadan sonra: 10, 20, 30. (c) Beş atama bitti. (d) Yazdırma döngüsünde i = 4: son geçerli hücre okunur. Bir sonraki turda i = 5 olur ve koşul yanlış çıkar.Tablo 3.3.3 döngünün altı kez koşul denetlediğini, gövdenin ise beş kez çalıştığını gösterir. Son denetimde i 5'tir ve 5 < 5 yanlıştır. Son geçerli indis length − 1 = 4'tür.
for (int i = 0; i < dizi.length; i++) kalıbı bir dizinin bütün indislerini 0'dan length − 1'e kadar dolaşır. Bu kalıp o kadar sık kullanılır ki ezbere yazılır. Kalıbın iki ayrıntısı önemlidir: başlangıç 0'dır ve karşılaştırma <'dir. <= yazmak sık yapılan bir hatadır ve son turda dizinin dışına çıkar.
Döngünün maliyeti de açıktır. Gövde her öğe için bir kez çalışır; n öğeli dizide n tur yapılır. İş miktarı dizinin boyutuyla doğru orantılı büyür. Bu maliyete O(n) denir.
3.3.2.5for-each döngüsü
Bir dizinin bütün öğelerini sırayla işlemek o kadar yaygındır ki Java bunun için ayrı bir döngü sunar. Bu döngüye gelişmiş for (enhanced for) ya da kısaca for-each döngüsü denir.
for (tür eleman : dizi) { … } biçimindeki for-each döngüsü, her turda dizinin sıradaki öğesinin bir kopyasını eleman değişkenine koyar ve gövdeyi çalıştırır. Döngü dizideki öğe sayısı kadar tur yapar. İndis yazılmaz.
İki nokta okunuşu kolaylaştırır. İki nokta üst üste “içindeki her … için” diye okunur: for (int not : notlar), “notlar içindeki her not için” demektir. Döngü değişkeninin tipi dizinin öğe tipiyle uyumlu olmalıdır. double[] dizisi int değişkenle dolaşılamaz, çünkü her turda bir daraltma dönüşümü gerekir:
E_ForEachTip.java:4: error: incompatible types: possible lossy conversion from double to int
for (int x : d) {
^
1 errorAşağıdaki program aynı diziyi önce klasik for, sonra for-each ile dolaşır:
int[] notlar = {70, 85, 90};
// Klasik for: indis elimizde
for (int i = 0; i < notlar.length; i++) {
System.out.println(i + ". not: " + notlar[i]);
}
// for-each: yalnızca değerler
int toplam = 0;
for (int not : notlar) {
toplam += not;
}
System.out.println("Toplam: " + toplam);0. not: 70
1. not: 85
2. not: 90
Toplam: 245Klasik for döngüsünde indis elimizdedir; çıktıdaki 0., 1., 2. sıra bilgisi bu sayede yazılır. for-each döngüsünde indis yoktur; yalnız değerler gelir. Toplam hesabı için indis gerekmediği için ikinci döngü daha kısadır ve daha az hataya açıktır.
for-each döngüsünün üç özelliği vardır. Birincisi, indis yazılmadığı için sınır dışı hatası yapılamaz. Döngü dizinin nerede bittiğini kendisi bilir. İkincisi, döngü yalnız okumak içindir. not = 0; yazmak dizideki öğeyi değiştirmez, yalnız kopyayı değiştirir. Üçüncüsü, indise ihtiyaç olan işler için uygun değildir. Diziye yazmak, öğenin konumunu bildirmek ya da iki komşu öğeyi karşılaştırmak indis ister. Bu durumlarda klasik for kullanılır.
3.3.2.6Kod izleme: değer gelir, hücre gelmez
for-each döngüsünün okuma amaçlı olduğunu en açık biçimde şu program gösterir:
int[] puanlar = {60, 75, 95};
int toplam = 0;
for (int p : puanlar) {
toplam += p;
}
System.out.println("Toplam: " + toplam);
for (int p : puanlar) {
p = 0;
}
System.out.println(puanlar[0] + " " + puanlar[2]);Program Toplam: 230 ve ardından 60 95 yazar. İlk döngü üç puanı toplar. İkinci döngü her öğeyi sıfırlamaya çalışır, ama dizi değişmez.
| Döngü | Tur | p | Gövde | toplam | puanlar |
|---|---|---|---|---|---|
| — | başlangıç | — | — | 0 | [60, 75, 95] |
| 1 | 1 | 60 | toplam += p | 60 | [60, 75, 95] |
| 1 | 2 | 75 | toplam += p | 135 | [60, 75, 95] |
| 1 | 3 | 95 | toplam += p | 230 | [60, 75, 95] |
| 2 | 1 | 60 → 0 | p = 0 | 230 | [60, 75, 95] |
| 2 | 2 | 75 → 0 | p = 0 | 230 | [60, 75, 95] |
| 2 | 3 | 95 → 0 | p = 0 | 230 | [60, 75, 95] |
for-each izlemesinden dört an. (a) İlk tur: puanlar[0]'ın kopyası p'ye gelir, p = 60. (b) Son tur sonunda toplam = 230. (c) İkinci döngüde p = 0 yalnız kopyayı değiştirir; dizideki 60 yerinde durur. (d) Döngüler bitince dizi hiç değişmemiştir.Şekil 3.3.6 nedeni görsel olarak verir. p, dizinin hücresi değil, ayrı bir yerel değişkendir. Her turun başında döngü sıradaki hücrenin değerini p'ye kopyalar. p = 0 bu yerel değişkene yazar; hücre ile p arasında bir bağ kalmamıştır. Diziye yazmak için klasik for ve puanlar[i] = 0 gerekir.
int[] x = {5, 6, 7}; dizisinin her öğesi ikiyle çarpılsın.
Yanlış yol: for (int v : x) v = v * 2;. Turlarda v sırasıyla 10, 12 ve 14 olur, ama bu değerler kopyalara yazılır. Arrays.toString(x) hâlâ [5, 6, 7] verir.
Doğru yol: for (int i = 0; i < x.length; i++) x[i] = x[i] * 2;. Bu kez hücrelerin kendisine yazılır ve dizi [10, 12, 14] olur.
3.3.2.7Sıcaklık dönüşümü
Diziler ile for-each döngüsü birlikte tablo üretmenin kısa bir yolunu verir. Aşağıdaki program dört Celsius değerini Fahrenhayt ve Kelvin'e çevirir:
double[] celsiuslar = {-40, 0, 25, 100};
for (double celsius : celsiuslar) {
double fahrenheit = (celsius * 1.8) + 32;
double kelvin = celsius + 273.15;
System.out.println(celsius + " °C = " + fahrenheit
+ " °F = " + kelvin + " K");
}-40.0 °C = -40.0 °F = 233.14999999999998 K
0.0 °C = 32.0 °F = 273.15 K
25.0 °C = 77.0 °F = 298.15 K
100.0 °C = 212.0 °F = 373.15 Kcelsiuslar dizisi. int olarak yazılan değerler double'a genişler ve -40.0, 0.0, 25.0, 100.0 olarak saklanır.double[] celsiuslar = {…}; dört değeri bir dizide toplar. Liste int sabitlerle yazılmıştır, ama dizi double olduğu için değerler -40.0, 0.0, 25.0 ve 100.0 olarak saklanır. Çıktıdaki .0'lar bundandır. for (double celsius : celsiuslar) “dizideki her öğe için” demektir. Gövde dört kez çalışır ve tablo tek seferde üretilir.
| Tur | celsius | celsius * 1.8 + 32 | celsius + 273.15 |
|---|---|---|---|
| 1 | -40.0 | -72.0 + 32 = -40.0 | 233.14999999999998 |
| 2 | 0.0 | 0.0 + 32 = 32.0 | 273.15 |
| 3 | 25.0 | 45.0 + 32 = 77.0 | 298.15 |
| 4 | 100.0 | 180.0 + 32 = 212.0 | 373.15 |
İlk satırdaki 233.14999999999998 bir hata gibi görünür, ama Java doğru çalışır. Ondalıklı sayılar bellekte ikili tabanda saklanır. 273.15 gibi pek çok ondalık sayının ikili tabanda sonlu bir karşılığı yoktur. Sayı en yakın temsil edilebilir değerle saklanır ve işlem sonucunda küçük bir yuvarlama farkı görünür. Aynı nedenle 0.1 + 0.2 de 0.30000000000000004 verir.
Bu fark yazdırırken basamak sayısı sınırlanarak gizlenebilir: System.out.printf("%.2f K%n", kelvin). Burada bir Türkiye ayrıntısı vardır. printf sayıyı bilgisayarın dil ayarına göre biçimlendirir. Türkçe ayarlı bir bilgisayarda çıktı 233,15 K olur; ondalık ayırıcı virgüldür. println ise ayardan bağımsız olarak her zaman nokta kullanır.
Ondalıklı sayıları == ile karşılaştırmak. -40 + 273.15 == 233.15 ifadesi false verir. Ondalıklı iki sayının eşitliği, aralarındaki farkın küçük bir eşikten az olmasıyla denetlenir: Math.abs(a - b) < 1e-9.
3.3.2.8Dizi değişkeni bir referanstır
Dizilerin en önemli ve en çok yanlış anlaşılan özelliği budur. int x = 5; yazıldığında x değişkeninin içinde 5 sayısı durur. int[] a = {1, 2, 3}; yazıldığında ise a'nın içinde üç sayı durmaz. Üç sayı heap'teki bir dizidedir; a o dizinin adresini, yani referansını tutar.
Referans (reference), bir nesnenin bellekteki yerini gösteren değerdir. Diziler, dizgiler ve bütün sınıf nesneleri referans tipidir: bu tiplerdeki değişkenler nesnenin kendisini değil, nesneye referansı tutar. İki değişken aynı nesneyi gösteriyorsa birine takma ad (alias) denir.
int[] a = {1, 2, 3};
int[] b = a; // kopya değil!
b[0] = 99;
System.out.println(a[0]);
int[] c = a.clone(); // gerçek kopya
c[1] = 77;
System.out.println(a[1] + " " + c[1]);
System.out.println(a == b);
System.out.println(a == c);Program dört satır yazar: 99, 2 77, true ve false. Her satırın nedeni Şekil 3.3.8'de görülür.
a onun referansını tutar. (b) b = a yalnız referansı kopyalar; iki değişken aynı diziyi gösterir. (c) b[0] = 99 ortak diziyi değiştirdiği için a[0] da 99 olur. (d) clone() heap'te yeni bir dizi oluşturur; c[1] = 77 yalnız bu kopyayı değiştirir.Satırları sırayla izleyelim. int[] b = a; yeni bir dizi oluşturmaz. Sağ taraftaki a'nın değeri bir referanstır ve bu referans b'ye kopyalanır. Artık iki değişken aynı diziyi gösterir. b[0] = 99 ortak dizinin ilk hücresine yazar. a[0] okunduğunda aynı hücreye bakıldığı için 99 gelir.
a.clone() ise gerçek bir kopya üretir. Heap'te yeni bir dizi oluşturulur ve öğeler oraya kopyalanır. Kopyalama anında a[0] 99 olduğu için kopya [99, 2, 3]'tür. c[1] = 77 yalnız kopyayı değiştirir; asıl dizide a[1] hâlâ 2'dir.
Son iki satır == operatörünün diziler üzerindeki anlamını gösterir. == referansları, yani adresleri karşılaştırır. a == b doğrudur, çünkü ikisi aynı diziyi gösterir. a == c yanlıştır. c'deki dizi o anda a'nınkiyle aynı içeriği taşısa bile farklı bir nesnedir. İçerik karşılaştırması için Arrays.equals kullanılır.
Diziyi metoda geçirmek
Java'da bütün argümanlar değer ile aktarılır (pass by value): metoda argümanın bir kopyası gider. Bir int geçirildiğinde sayının kopyası gider. Bir dizi geçirildiğinde ise referansın kopyası gider. Kopya referans da aynı diziyi gösterir. Bu yüzden metot dizinin hücrelerini değiştirirse değişiklik çağıranın dizisinde görünür.
Şu metot ve çağrı verilsin:
static void degistir(int[] d, int x) { d[0] = 100; x = 100; }int[] a = {1, 2, 3}; int x = 1; degistir(a, x);
Çağrıdan sonra a[0] 100, x ise 1'dir. d, a'daki referansın kopyasıdır ve aynı diziyi gösterir; d[0] = 100 o dizinin hücresine yazar. Metottaki x ise main'deki x'in kopyasıdır. Kopyaya 100 yazmak asıl değişkeni etkilemez.
Bu kuralın bir sonucu daha vardır. Metot içinde parametreye yeni bir dizi atanırsa, örneğin d = new int[]{0, 0};, yalnız yerel kopya yeni diziye bağlanır. Çağıranın değişkeni eski diziyi göstermeye devam eder. Metot bundan sonra d'ye ne yazarsa yeni diziye yazar ve çağıran bunu görmez.
Atamayı kopya sanmak. int[] yedek = notlar; yazılıp sonra notlar sıralanırsa yedek de sıralanmış olur, çünkü ortada tek bir dizi vardır. Gerçek kopya için notlar.clone() ya da Arrays.copyOf(notlar, notlar.length) kullanılır.
3.3.2.9java.util.Arrays sınıfı
Dizilerle sık yapılan işler için Java hazır metotlar sunar. Bu metotlar java.util.Arrays sınıfındadır ve kullanmak için dosyanın başına import java.util.Arrays; yazılır. Metotların hepsi static'tir; Arrays.sort(d) biçiminde sınıf adıyla çağrılır.
int[] dizi = {5, 3, 8, 1};
System.out.println(Arrays.toString(dizi));
Arrays.sort(dizi);
System.out.println(Arrays.toString(dizi));
int[] kopya = Arrays.copyOf(dizi, 6);
System.out.println(Arrays.toString(kopya));
int[] dolu = new int[4];
Arrays.fill(dolu, 7);
System.out.println(Arrays.toString(dolu));
int[] x = {1, 2};
int[] y = {1, 2};
System.out.println((x == y) + " " + Arrays.equals(x, y));
System.out.println(Arrays.binarySearch(dizi, 5));[5, 3, 8, 1]
[1, 3, 5, 8]
[1, 3, 5, 8, 0, 0]
[7, 7, 7, 7]
false true
2Satır satır: Arrays.toString diziyi okunabilir bir metne çevirir. Arrays.sort diziyi küçükten büyüğe sıralar. Sıralama yerinde yapılır; yeni dizi dönmez, verilen dizi değişir. Arrays.copyOf(dizi, 6) 6 hücreli yeni bir dizi oluşturur ve ilk dört hücreye eski öğeleri kopyalar; fazla hücreler varsayılan değer 0'ı alır. Yeni uzunluk eskisinden küçük verilirse dizi kesilir: copyOf(dizi, 2) [1, 3] verir.
Arrays.fill(dolu, 7) bütün hücrelere aynı değeri yazar. x ile y aynı öğeleri taşıyan iki ayrı dizidir: x == y yanlış, Arrays.equals(x, y) doğrudur. Son satırdaki Arrays.binarySearch(dizi, 5) sıralı dizide 5'i arar ve indisini verir: 2.
| Metot | Görevi |
|---|---|
Arrays.toString(d) | Diziyi [5, 3, 8, 1] biçiminde metne çevirir. |
Arrays.deepToString(m) | İç içe dizileri (matrisleri) metne çevirir. |
Arrays.sort(d) | Diziyi yerinde, küçükten büyüğe sıralar. |
Arrays.binarySearch(d, x) | Sıralı dizide x'in indisini verir; yoksa negatif bir sayı döner. |
Arrays.copyOf(d, n) | n hücreli yeni dizi oluşturup kopyalar; fazla hücreler varsayılan değeri alır. |
Arrays.fill(d, v) | Bütün hücrelere v yazar. |
Arrays.equals(a, b) | İki dizinin uzunluğunu ve öğelerini karşılaştırır. |
Diziyi doğrudan println ile yazdırmak beklenen sonucu vermez. System.out.println(dizi) öğeleri değil, [I@1c20c684 gibi bir kimlik yazar. Baştaki [I “int dizisi” demektir; @'ten sonraki onaltılık sayı nesnenin kimlik kodudur ve her çalıştırmada değişebilir. Öğeleri görmek için Arrays.toString(dizi) kullanılır.
İki ayrıntı daha bilinmelidir. Birincisi, dizilerde indexOf gibi bir arama metodu yoktur. Sırasız bir dizide arama için döngü yazılır; bu yönteme doğrusal arama denir. İkincisi, binarySearch yalnız sıralı dizide doğru çalışır. Aranan değer yoksa sonuç, değerin ekleneceği konumu da kodlayan negatif bir sayıdır. Sıralı [1, 3, 5, 8] dizisinde 4 aranırsa sonuç −3'tür. Sıralanmamış dizide sonuç anlamsızdır ve bir uyarı da verilmez.
Metotların maliyeti farklıdır. toString, fill, copyOf ve equals her öğeye bir kez dokunduğu için O(n)'dir. binarySearch O(log n), sort ise ortalamada O(n log n)'dir. sort'un bir satırlık yazımı, arkasındaki işin küçük olduğu anlamına gelmez.
3.3.3Bellek ve hatalar
Dizi kullanan programlarda en sık görülen çalışma zamanı hatası, dizinin sınırları dışına çıkmaktır. Bu hatayı anlamak için önce dizinin bellekte nerede durduğu bilinmelidir. Kesim, Java'nın belleği nasıl böldüğünü, kullanılmayan dizilerin nasıl temizlendiğini ve sınır hatalarının nasıl yakalandığını anlatır.
3.3.3.1Diziler bellekte nerede?
Çalışan bir Java programının belleği iki ana bölgeye ayrılır. Yığın (stack), metot çağrılarının yerel değişkenlerini tutar. Her metot çağrısı yığında bir çerçeve açar; metot bitince çerçeve kalkar ve içindeki değişkenler yok olur. Heap ise new ile oluşturulan nesnelerin durduğu bölgedir. Heap'teki bir nesne, onu oluşturan metot bittikten sonra da yaşayabilir.
Java'da bütün diziler heap'tedir. Yığında yalnız dizileri gösteren referans değişkenleri durur. C ve C++ dillerinde durum farklıdır. int a[10]; biçiminde tanımlanan dizi yığında durur. malloc ya da new ile ayrılan dizi heap'tedir ve işi bitince programcı onu elle serbest bırakır. Java'da bu ayrım yoktur; serbest bırakma da otomatiktir.
String[] plaka = {"", "Adana", "Adıyaman", "Afyon"};
System.out.println(plaka[1]);
int[] dinamikDizi = new int[10]; // heap'te yer ayrılır
System.out.println(dinamikDizi.length);
dinamikDizi = null; // referans koparıldı
System.out.println(dinamikDizi == null);Program sırasıyla Adana, 10 ve true yazar. plaka dizisinin ilk hücresi boş dizgidir; böylece plaka kodu 1 olan il plaka[1]'de durur ve indis ile plaka kodu örtüşür. Bu, indisin 0'dan başlamasıyla uzlaşmanın pratik bir yoludur. dinamikDizi'nin boyutu çalışma zamanında da verilebilirdi; ama dizi oluştuktan sonra boyutu sabittir.
String[] hücreleri de aslında ayrı String nesnelerine referanstır. (b) dinamikDizi = null bağı koparır; int dizisi erişilemez olur. (c) Metot bitince yığın çerçevesi kalkar; başka referansı kalmayan diziler de zamanla toplanır.dinamikDizi = null; satırı diziyi silmez. Yalnız değişken ile dizi arasındaki bağı koparır. Şekil 3.3.9'da görüldüğü gibi dizi heap'te durmaya devam eder, ama artık ona ulaşan bir referans yoktur. Böyle erişilemez nesneleri JVM'in çöp toplayıcısı (garbage collector) bulur ve belleklerini geri alır.
Çöp toplayıcı, hiçbir referansın göstermediği nesneleri bulup kapladıkları belleği geri veren JVM bileşenidir. Java'da free ya da delete gibi bir serbest bırakma komutu yoktur; bellek yönetimi otomatiktir.
Çöp toplayıcının ne zaman çalışacağı belirli değildir. Bir dizinin erişilemez olması, belleğin hemen boşalacağı anlamına gelmez. JVM uygun bir anda toplama yapar. Programcı açısından önemli olan, gereksiz referansları tutmamaktır. Büyük bir diziyi gösteren bir değişken programın sonuna kadar yaşarsa dizi de yaşar.
int[] p = new int[1000]; int[] q = p; p = new int[5]; q = null; satırlarından sonra ilk dizi ne olur?
İlk satırda 1000 hücreli dizi oluşur ve p onu gösterir. İkinci satırda q da aynı diziyi gösterir. Üçüncü satır p'yi yeni, 5 hücreli bir diziye bağlar; ilk diziyi hâlâ q gösterir. Dördüncü satır bu son bağı da koparır.
Artık 1000 hücreli diziye ulaşan referans kalmamıştır ve çöp toplayıcı onu toplayabilir. Dizi küçülmemiştir: p = new int[5] yeni bir dizi oluşturur, eskisini değiştirmez.
3.3.3.2İndis sınır dışı hatası
Dizinin geçerli indisleri 0 ile length − 1 arasındadır. Bu aralığın dışındaki bir indisle erişim yapılırsa Java çalışma zamanında ArrayIndexOutOfBoundsException fırlatır. Bu istisna, IndexOutOfBoundsException sınıfının bir alt sınıfıdır. İstisna yakalanmazsa program o satırda durur.
ArrayIndexOutOfBoundsException, bir diziye negatif ya da length'e eşit veya ondan büyük bir indisle erişildiğinde JVM'in fırlattığı istisnadır. Derleyici indis değerlerini denetlemez; denetim her erişimde çalışma zamanında yapılır.
Bu davranış Java'yı C'den ayırır. C'de a[10] gibi bir erişim denetlenmez. Program dizinin hemen arkasındaki belleği okur ya da oraya yazar. Sonuç rastgele bir değer, başka bir değişkenin bozulması ya da çok sonra ortaya çıkan anlaşılmaz bir hatadır. Java ise hatayı tam oluştuğu satırda yakalar ve programı durdurur. Program çöker, ama sessizce yanlış sonuç üretmez.
int[] dizi = {10, 20, 30, 40, 50};
int index = 5;
// Dizi uzunluğunu kontrol etme
if (index >= 0 && index < dizi.length) {
int deger = dizi[index];
System.out.println("Değer: " + deger);
} else {
System.out.println("Geçersiz indis: " + index);
}
// Kontrolsüz erişim:
System.out.println(dizi[index]);Geçersiz indis: 5
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
at SinirDisi.main(SinirDisi.java:13)Programın ilk kısmı güvenli erişimin kalıbını gösterir: indis kullanılmadan önce index >= 0 && index < dizi.length koşuluyla denetlenir. 5 geçerli bir indis olmadığı için else dalı çalışır ve Geçersiz indis: 5 yazılır. Son satır ise aynı indisi denetimsiz kullanır ve istisna oluşur.
İstisna iletisi hatayı bulmak için gereken her şeyi söyler. Index 5 hatalı indistir. out of bounds for length 5 dizinin uzunluğudur. at SinirDisi.main(SinirDisi.java:13) satırı hatanın hangi sınıfta, hangi metotta ve dosyanın kaçıncı satırında oluştuğunu verir. Bir sınır hatasıyla karşılaşıldığında ilk bakılacak yer bu satırdır.
Negatif indis de aynı istisnayı doğurur. Bazı dillerde d[-1] son öğe anlamına gelir; Java'da gelmez:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 3
at Negatif.main(Negatif.java:4)Son indisi length sanmak. a[a.length] her zaman sınır dışıdır; son öğe a[a.length - 1]'dir. Aynı hata döngü koşulunda i <= a.length olarak görünür. Üç öğeli bir dizide ileti Index 3 out of bounds for length 3 olur: hatalı indis ile uzunluk eşittir.
3.3.3.3Klasik hata: <= ile döngü
Sınır dışı hatasının en yaygın kaynağı döngü koşuludur:
int[] dizi = new int[3];
for (int i = 0; i <= dizi.length; i++) {
dizi[i] = i * 10;
}Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
at SinirDongu.main(SinirDongu.java:5)i = 3 olduğunda koşul 3 <= 3 doğru kalır ve var olmayan dördüncü hücreye, dizi[3]'e yazılmaya çalışılır.Döngü i = 0, 1, 2 için hücrelere 0, 10 ve 20 yazar. i 3 olduğunda 3 <= 3 doğrudur ve gövde bir kez daha çalışır. dizi[3] yoktur ve istisna fırlatılır. Doğru koşul i < dizi.length'tir. Sabit bir sayı yazmak, örneğin i < 3, bu dizi için doğru sonuç verir; ama dizinin boyutu değişirse kod bozulur. length kullanmak koşulu diziye bağlar.
Hatanın hangi turda oluştuğunu görmek için benzer bir programı adım adım izleyelim:
int[] kare = new int[4];
for (int i = 0; i <= kare.length; i++) {
kare[i] = i * i;
}
System.out.println("bitti");i | i <= kare.length | Gövde | kare |
|---|---|---|---|
| — | — | başlangıç | [0, 0, 0, 0] |
| 0 | 0 <= 4 doğru | kare[0] = 0 | [0, 0, 0, 0] |
| 1 | 1 <= 4 doğru | kare[1] = 1 | [0, 1, 0, 0] |
| 2 | 2 <= 4 doğru | kare[2] = 4 | [0, 1, 4, 0] |
| 3 | 3 <= 4 doğru | kare[3] = 9 | [0, 1, 4, 9] |
| 4 | 4 <= 4 doğru | kare[4] yok: istisna | program durur |
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4
at Kare.main(Kare.java:5)Tablo 3.3.7 hatanın tam yerini gösterir. Son geçerli hücre dördüncü turda dolar. Beşinci turda 4 <= 4 de doğrudur; < yazılsaydı döngü burada biterdi. JVM kare[4] erişiminde istisna fırlatır ve program bu satırda durur. "bitti" hiç yazılmaz, çünkü istisna yakalanmadığı için sonraki satırlara geçilmez.
i = 3: kare[3] = 9 ile son geçerli hücre dolar. (b) i = 4: kare[4] yoktur; JVM ArrayIndexOutOfBoundsException fırlatır.Hatanın kökü sayma alışkanlığıdır. 1'den 4'e kadar sayan biri <= yazar; 0'dan başlayıp dört öğe sayan biri ise < 4 yazmalıdır. İki kalıp birbirine karıştırıldığında ya ilk öğe atlanır ya da son turda dizinin dışına çıkılır. for (int i = 1; i <= d.length; i++) ikisini birden yapar: d[0] hiç yazılmaz, son turda da istisna oluşur.
3.3.3.4Oluşturulmamış dizi: null
Bir dizi değişkeni tanımlanıp ona dizi bağlanmazsa ya da açıkça null atanırsa, değişken hiçbir diziyi göstermez. Böyle bir değişkenle hücreye erişmek NullPointerException doğurur:
Exception in thread "main" java.lang.NullPointerException: Cannot load from int array because "<local1>" is null
at Npe.main(Npe.java:4)Java 14'ten bu yana bu iletiler “yardımcı” biçimdedir: neyin null olduğunu da söyler. İletideki "<local1>", metodun 1 numaralı yerel değişkenidir, yani a'dır. Derleyici varsayılan olarak yerel değişken adlarını .class dosyasına yazmaz. Kod javac -g ile derlenirse ya da çoğu IDE'de olduğu gibi hata ayıklama bilgisi eklenirse ileti doğrudan because "a" is null olur.
Dizi ile null arasındaki fark, boş dizi ile yokluk arasındaki farktır. int[] a = new int[0]; bir dizidir ve a.length 0 verir. int[] a = null; ise dizi değildir; a.length bile NullPointerException fırlatır.
3.3.4İstatistik örnekleri
Bir veri kümesini özetlemenin en bilinen üç ölçüsü ortalama, ortanca ve moddur. Üçü de dizilerle doğal biçimde hesaplanır ve her biri farklı bir teknik gösterir. Ortalama tek bir geçişle toplar, ortanca sıralamaya dayanır, mod sıralı dizide komşuları karşılaştırır. Üç program da verileri kullanıcıdan okuyan aynı bölümle başlar.
3.3.4.1Kullanıcıdan dizi okuma
import java.util.Scanner;
public class OrtalamaHesaplama {
public static void main(String[] args) {
// Kullanıcıdan dizi boyutunu al
Scanner scanner = new Scanner(System.in);
System.out.print("boyutu girin: ");
int boyut = scanner.nextInt();
// Kullanıcıdan elemanları al ve dizi oluştur
double[] dizi = new double[boyut];
for (int i = 0; i < boyut; i++) {
System.out.print((i + 1) + ". elemani girin: ");
dizi[i] = scanner.nextDouble();
}
// ... diziyle hesaplamalar
}
}Örnek bir çalışmada kullanıcı önce boyutu, ardından dört değeri girer:
boyutu girin: 4
1. elemani girin: 7
2. elemani girin: 3
3. elemani girin: 9
4. elemani girin: 5Bu programda dizinin boyutu derleme sırasında bilinmez. scanner.nextInt() kullanıcının girdiği sayıyı okur ve new double[boyut] diziyi o boyutta oluşturur. Boyut çalışma zamanında belirlenir, ama dizi oluştuktan sonra değişmez. Kullanıcı daha fazla değer girmek isterse yeni bir dizi gerekir.
İstem satırındaki (i + 1) küçük ama önemli bir ayrıntıdır. Kullanıcı 1'den sayar ve “1. eleman” görmeyi bekler. Kod ise 0'dan sayar ve değeri dizi[i]'ye yazar. Ekranda i + 1, kodda i kullanılır. Parantezler de gereklidir: (i + 1) + ". elemani girin: " önce toplamayı yapar. Parantez olmasaydı i + 1 + "." soldan sağa değerlendirilir, yine doğru sonuç verirdi; ama "" + i + 1 gibi bir yazım "01" üretirdi. Parantez niyeti açıkça gösterir.
Scanner'ın nextDouble() metodu ondalık ayırıcıyı bilgisayarın dil ayarına göre okur. Türkçe ayarlı bir bilgisayarda 7,5 kabul edilir, 7.5 ise InputMismatchException doğurur. Tam sayı girişlerinde bu sorun çıkmaz.
3.3.4.2Ortalama
Ortalama (aritmetik ortalama), değerlerin toplamının değer sayısına bölümüdür. Program okuma bölümünden sonra iki döngü daha çalıştırır: biri toplar, öteki yazdırır.
Scanner scanner = new Scanner(System.in);
System.out.print("boyutu girin: ");
int boyut = scanner.nextInt();
double[] dizi = new double[boyut];
for (int i = 0; i < boyut; i++) {
System.out.print((i + 1) + ". elemani girin: ");
dizi[i] = scanner.nextDouble();
}
double toplam = 0;
for (int i = 0; i < boyut; i++) {
toplam += dizi[i];
}
double ortalama = toplam / boyut;
System.out.print("Elemanlar: ");
for (int i = 0; i < boyut; i++) {
System.out.print(dizi[i] + " ");
}
System.out.println();
System.out.println("Ortalama: " + ortalama);boyutu girin: 4
1. elemani girin: 7
2. elemani girin: 3
3. elemani girin: 9
4. elemani girin: 5
Elemanlar: 7.0 3.0 9.0 5.0
Ortalama: 6.0| Adım | i | İşlem | dizi | toplam |
|---|---|---|---|---|
| okuma | — | boyut = 4, new double[4] | [0.0, 0.0, 0.0, 0.0] | — |
| okuma | 0 | dizi[0] = 7.0 | [7.0, 0.0, 0.0, 0.0] | — |
| okuma | 1 | dizi[1] = 3.0 | [7.0, 3.0, 0.0, 0.0] | — |
| okuma | 2 | dizi[2] = 9.0 | [7.0, 3.0, 9.0, 0.0] | — |
| okuma | 3 | dizi[3] = 5.0 | [7.0, 3.0, 9.0, 5.0] | — |
| toplama | — | toplam = 0 | aynı | 0.0 |
| toplama | 0 | toplam += dizi[0] | aynı | 7.0 |
| toplama | 1 | toplam += dizi[1] | aynı | 10.0 |
| toplama | 2 | toplam += dizi[2] | aynı | 19.0 |
| toplama | 3 | toplam += dizi[3] | aynı | 24.0 |
| bölme | — | ortalama = 24.0 / 4 | aynı | 6.0 |
new double[boyut]: dört hücre de 0.0. (b) İki değer okundu. (c) Dört değer okundu: 7.0, 3.0, 9.0, 5.0. (d) Toplama döngüsünün son turu: toplam = 24.0.Bölmenin ondalıklı olması bir tasarım kararının sonucudur. toplam double tanımlandığı için toplam / boyut bir double ile bir int'in bölümüdür. int önce double'a genişler ve gerçek bölme yapılır: 24.0 / 4 = 6.0. Toplam int olsaydı fark görünürdü. 7, 3, 9, 5, 1, 4 kümesi için 29 / 6 tam sayı bölmesiyle 4 verir; doğrusu 4,833…'tür.
Ortalamayı iki int ile hesaplayıp sonucu double'a atamak: double ortalama = toplam / dizi.length;. Bölme, atamadan önce iki int arasında yapılır ve kesir zaten atılmıştır; 29 / 6 önce 4 olur, sonra 4.0'a dönüşür. Doğrusu bölmeden önce dönüşümdür: toplam / (double) dizi.length.
Programın maliyeti açıktır. Okuma, toplama ve yazdırma döngülerinin her biri n tur yapar; toplam iş 3n düzeyindedir ve bu O(n)'dir. Toplam okuma döngüsünde de biriktirilebilirdi; iki döngüyü birleştirmek turu azaltır ama maliyetin büyüme biçimini değiştirmez.
3.3.4.3Ortanca
Ortanca (medyan), sıralanmış bir veri kümesinin ortasındaki değerdir. Öğe sayısı tekse tam ortadaki öğe, çiftse ortadaki iki öğenin ortalamasıdır.
Ortanca, uç değerlerden ortalama kadar etkilenmez. 3, 4, 5, 6 ve 100 değerlerinin ortalaması 23,6'dır ve verilerin hiçbirine yakın değildir. Ortancası ise 5'tir. Gelir, ev fiyatı ya da ağ gecikmesi gibi birkaç aşırı değer içeren ölçümlerde ortanca bu yüzden tercih edilir.
// boyut ve dizi, OrtalamaHesaplama'daki gibi okunur
Arrays.sort(dizi); // Diziyi sırala
double ortanca;
if (boyut % 2 == 0) { // Çift sayıda eleman
int orta1 = boyut / 2 - 1;
int orta2 = boyut / 2;
ortanca = (dizi[orta1] + dizi[orta2]) / 2;
} else { // Tek sayıda eleman
ortanca = dizi[boyut / 2];
}
System.out.println("Ortanca Değer: " + ortanca);girilenler: 7 3 9 5 1
Elemanlar (sirali): 1.0 3.0 5.0 7.0 9.0
Ortanca Değer: 5.0Hesabın ilk adımı sıralamadır. Ortanca sıralamaya göre tanımlıdır; sırasız bir dizinin ortasındaki öğe rastgele bir değerdir. Şekil 3.3.13 bunu gösterir: girilen sırada ortadaki hücre 9.0'dır, ama ortanca 5.0'tır.
Arrays.sort sonrası, boyut 5 (tek): dizi[5 / 2] = dizi[2] = 5.0. (c) Boyut 4 (çift; girdiler 7 3 9 5): orta1 = 1, orta2 = 2 ve ortanca (5.0 + 7.0) / 2 = 6.0.Tek boyutta ortadaki indis boyut / 2'dir. Boyut 5 için 5 / 2 tam sayı bölmesiyle 2 verir; 2,5 değil. Tam sayı bölmesinin kesir atması burada işe yarar. 0'dan başlayan indislerle 5 öğenin ortası gerçekten indis 2'dedir: solunda iki, sağında iki öğe vardır.
Çift boyutta tek bir orta öğe yoktur. Boyut 4 için ortadaki iki indis 4 / 2 − 1 = 1 ve 4 / 2 = 2'dir. Girdiler 7, 3, 9, 5 ise sıralı dizi [3.0, 5.0, 7.0, 9.0]'dır ve ortanca (5.0 + 7.0) / 2 = 6.0 olur. Dizi double olduğu için bölme ondalıklıdır. Aynı kod int diziyle yazılsaydı (4 + 5) / 2 gibi bir ifade 4 verirdi; doğrusu 4,5'tir ve / 2.0 yazmak gerekir.
Koddaki double ortanca; satırı dalların dışındadır. Bu yerleşim zorunludur. Değişken if ve else dallarının içinde tanımlansaydı her biri yalnız kendi bloğunda yaşardı. Dalların dışındaki println onu göremezdi:
E_Ortanca.java:10: error: cannot find symbol
System.out.println("Ortanca Değer: " + ortanca);
^
symbol: variable ortanca
location: class E_Ortanca
1 errorDeğişkeni dışarıda tanımlayıp yalnız bir dalda değer vermek de yetmez. else dalı olmasaydı derleyici, if koşulu yanlışken değişkenin değersiz kalacağını görür ve variable ortanca might not have been initialized hatasını verirdi. Koddaki biçimde her iki dal da değer atadığı için sorun yoktur.
Arrays.sort bir yan etki taşır: diziyi yerinde değiştirir. Ortanca hesaplandıktan sonra kullanıcının girdiği sıra kaybolmuştur. Sıra daha sonra gerekiyorsa sıralama bir kopya üzerinde yapılır: double[] sirali = dizi.clone(); Arrays.sort(sirali);.
Maliyeti sıralama belirler. Ortadaki öğeyi okumak O(1)'dir, ama Arrays.sort O(n log n) iş yapar. Ortanca, ortalamadan daha pahalı bir ölçüdür.
3.3.4.4Mod
Mod (tepe değer), bir veri kümesinde en sık tekrar eden değerdir. 4 2 7 2 4 2 9 kümesinin modu 2'dir; 2 üç kez geçer.
Modu bulmanın yalın bir yolu, her değer için kaç kez geçtiğini bütün diziyi tarayarak saymaktır. Bu yol n değerin her biri için n karşılaştırma yapar ve O(n²) maliyetlidir. Sunumdaki program daha akıllı bir yol izler. Önce diziyi sıralar; böylece eşit değerler yan yana gelir. Sonra tek bir geçişte en uzun “seriyi” bulur.
int enCokTekrarEden = dizi[0];
int enCokTekrarSayisi = 1;
int mevcutTekrarEden = dizi[0];
int mevcutTekrarSayisi = 1;
for (int i = 1; i < boyut; i++) {
if (dizi[i] == dizi[i - 1]) {
mevcutTekrarSayisi++;
} else {
mevcutTekrarSayisi = 1;
mevcutTekrarEden = dizi[i];
}
if (mevcutTekrarSayisi > enCokTekrarSayisi) {
enCokTekrarSayisi = mevcutTekrarSayisi;
enCokTekrarEden = mevcutTekrarEden;
}
}Dört değişken iki seriyi izler. mevcutTekrarEden ve mevcutTekrarSayisi o anda sayılan serinin değeri ve uzunluğudur. enCokTekrarEden ve enCokTekrarSayisi şimdiye kadar görülen en uzun serinin değeri ve uzunluğudur. Döngü i = 1'den başlar, çünkü her öğe kendisinden önceki öğeyle karşılaştırılır ve ilk öğenin öncesi yoktur. İlk öğe başlangıçta hem mevcut hem en uzun seri sayılır.
Her turda iki karar verilir. Birincisi: öğe bir öncekine eşitse seri uzar; değilse yeni bir seri başlar ve uzunluk 1 olur. İkincisi: mevcut seri en uzun seriden uzunsa en uzun seri güncellenir. Sonuç şöyle yazdırılır:
// Sonucu yazdır
System.out.println("Elemanlar sirali: " + Arrays.toString(dizi));
System.out.print("Mod: " + enCokTekrarEden);
System.out.println(" (Tekrar Sayisi: " + enCokTekrarSayisi + ")");Elemanlar sirali: [2, 2, 2, 4, 4, 7, 9]
Mod: 2 (Tekrar Sayisi: 3)i = 3: dizi[3] = 4, öncekinden (2) farklıdır ve yeni bir seri başlar.i | Karşılaştırma | Mevcut seri | En uzun seri |
|---|---|---|---|
| — | başlangıç | 2 × 1 | 2 × 1 |
| 1 | 2 == 2 | 2 × 2 | 2 × 2 (güncellendi) |
| 2 | 2 == 2 | 2 × 3 | 2 × 3 (güncellendi) |
| 3 | 4 ≠ 2 | 4 × 1 | 2 × 3 |
| 4 | 4 == 4 | 4 × 2 | 2 × 3 |
| 5 | 7 ≠ 4 | 7 × 1 | 2 × 3 |
| 6 | 9 ≠ 7 | 9 × 1 | 2 × 3 |
| 7 | 7 < 7 yanlış: döngü biter. Mod 2, tekrar sayısı 3. | ||
Güncelleme koşulundaki > işareti eşitlik durumunu belirler. İki seri aynı uzunluktaysa sonradan gelen seri öncekini geçemez; ilk bulunan korunur. Dizi sıralı olduğu için bu, eşit sıklıktaki değerlerden en küçüğünün seçilmesi demektir. >= yazılsaydı son bulunan, yani en büyüğü seçilirdi. Birden çok mod olabileceğini bildirmek gerekiyorsa ayrıca saymak gerekir.
Sıralı {2, 2, 6, 6, 6, 9} dizisinde mod nedir?
İlk seri 2 × 2'dir; en uzun seri 2 × 2 olur. i = 2'de 6 başlar (6 × 1). i = 3'te seri 6 × 2 olur, ama 2 > 2 yanlıştır ve en uzun seri 2 olarak kalır. i = 4'te seri 6 × 3 olur. 3 > 2 doğrudur ve en uzun seri güncellenir. 9 yeni bir seri başlatır.
Program Mod: 6 (3) yazar.
Bu yöntemin maliyeti iki parçadır: O(n log n) sıralama ve O(n) tarama. Toplam O(n log n)'dir ve yalın sayma yönteminin O(n²) maliyetinden çok daha iyidir. Sıralama burada bir hazırlık adımıdır; ardından gelen hesap, eşit değerlerin yan yana durmasına dayanır.
3.3.5Dizi algoritmaları
Dizi üzerinde en sık yapılan işler aramak, karşılaştırmak, sıralamak ve sırayı değiştirmektir. Bu kesim bu işlerin temel algoritmalarını verir ve her birinin ne kadar iş yaptığını sayar. Sayma işi, algoritmaları karşılaştırmanın ortak dili olan O gösterimine götürür.
3.3.5.1Doğrusal arama
Doğrusal arama (linear search), aranan değeri dizinin başından sonuna kadar her öğeyle sırayla karşılaştıran arama yöntemidir. Değer bulununca arama biter; dizinin sonuna gelinirse değer yoktur.
static boolean lineerArama(int[] dizi, int hedef) {
for (int eleman : dizi) {
if (eleman == hedef) {
return true;
}
}
return false;
}
// main içinde:
int[] dizi = {10, 20, 30, 40, 50};
boolean sonuc = lineerArama(dizi, 30);lineerArama metodu iki parametre alır: aranacak dizi ve hedef değer. for-each döngüsü öğeleri sırayla eleman'a koyar. Bir öğe hedefe eşitse return true hem döngüyü hem metodu hemen bitirir. Döngü hiçbir eşitlik bulmadan biterse son satıra gelinir ve false döner. Arama indis gerektirmediği için for-each burada uygundur.
| Tur | eleman | eleman == hedef | Sonuç |
|---|---|---|---|
| 1 | 10 | 10 == 30 yanlış | sıradaki öğe |
| 2 | 20 | 20 == 30 yanlış | sıradaki öğe |
| 3 | 30 | 30 == 30 doğru | return true |
eleman = 10, hedef değil. (b) Üçüncü tur: 30 bulunur ve metot true döndürür. 40 ve 50'ye hiç bakılmaz.main, lineerArama(dizi, 30)'u çağırdığında metoda dizinin referansı ve 30 değeri gider. Dizi kopyalanmaz. Üç karşılaştırma sonucu bulmaya yeter; metot çerçevesi yığından kalkar ve sonuc true olur. Hedef 35 olsaydı beş karşılaştırmanın hepsi yanlış çıkar ve metot false döndürürdü.
Aynı arama metot yazmadan da yapılabilir. Bu kez döngüden çıkmak için break kullanılır:
int[] dizi = {10, 20, 30, 40, 50};
// Bir dizide belirli bir elemanı arama
int aranan = 30;
boolean bulundu = false;
for (int eleman : dizi) {
if (eleman == aranan) {
bulundu = true;
break;
}
} // Bu işlem O(n) karmaşıklığına sahiptir.break, hedef bulunduğu anda döngüden çıkar. Bu örnekte yalnız üç öğeye bakılır ve bulundu true olur. break olmasaydı sonuç yine doğru olurdu, ama döngü 40 ve 50'ye de gereksiz yere bakardı. bulundu değişkeninin false ile başlaması önemlidir: hiçbir eşitlik bulunmazsa doğru cevap “yok”tur.
Arama sonucunda yalnız “var mı?” sorusunun cevabı yetmeyebilir. Öğenin nerede olduğu gerekiyorsa indis döndürülür ve bu kez klasik for kullanılır: for (int i = 0; i < d.length; i++) if (d[i] == hedef) return i; return -1;. −1 geçerli bir indis olamayacağı için “yok” anlamında kullanılır. {12, 7, 30, 5, 18} dizisinde 5 aranırsa sonuç 3, 99 aranırsa −1'dir.
3.3.5.2Doğrusal aramanın maliyeti
Bir algoritmanın hızını saniyeyle ölçmek yanıltıcıdır; aynı kod hızlı bir bilgisayarda daha kısa sürer. Bunun yerine algoritmanın yaptığı temel işlem sayılır. Doğrusal aramada temel işlem bir karşılaştırmadır.
Karşılaştırma sayısı hedefin nerede olduğuna bağlıdır. İyi durumda hedef ilk öğedir ve tek karşılaştırma yeter. Kötü durumda hedef dizinin sonundadır ya da hiç yoktur; n öğeli dizide n karşılaştırma yapılır. Hedef dizide rastgele bir yerdeyse ortalama karşılaştırma sayısı yaklaşık n/2'dir.
Zaman karmaşıklığı (time complexity), bir algoritmanın yaptığı iş miktarının girdi boyutu n ile nasıl büyüdüğünü anlatır. O gösterimi bu büyümenin biçimini verir; sabit çarpanları ve küçük terimleri atar. İş miktarı n ile doğru orantılı büyüyorsa algoritma O(n)'dir.
Doğrusal aramanın zaman karmaşıklığı O(n)'dir. n/2 ile n arasındaki fark O gösteriminde önemsizdir: ikisi de dizi iki katına çıkınca iki katına çıkar. Sırasız bir dizide daha iyisi yapılamaz. Her öğeye bakmadan “yok” denemez, çünkü bakılmayan öğe hedef olabilir.
3.3.5.3İkili arama
Dizi sıralıysa durum değişir. Telefon rehberinde bir ad ararken kimse ilk sayfadan başlamaz. Rehber ortadan açılır; aranan ad o sayfadakinden önce geliyorsa sol yarıya, sonra geliyorsa sağ yarıya geçilir. Her bakışta aranacak alan yarıya iner.
İkili arama (binary search), sıralı bir dizide ortadaki öğeye bakıp hedef küçükse sol yarıda, büyükse sağ yarıda aramayı süren yöntemdir. Arama alanı her adımda yarıya iner. Yalnız sıralı dizide çalışır.
static int ikiliAra(int[] dizi, int hedef) {
int alt = 0, ust = dizi.length - 1;
while (alt <= ust) {
int orta = (alt + ust) / 2;
if (dizi[orta] == hedef) {
return orta; // bulundu
} else if (hedef < dizi[orta]) {
ust = orta - 1; // sol yarı
} else {
alt = orta + 1; // sağ yarı
}
}
return -1; // alan tükendi
}
// main içinde:
int[] dizi = {3, 9, 14, 27, 42, 48, 55, 61, 70};
System.out.println(ikiliAra(dizi, 48));
System.out.println(ikiliAra(dizi, 50));5
-1Metot arama alanını iki değişkenle tutar: alt alanın ilk, ust son indisidir. Başlangıçta alan bütün dizidir. Her turda ortadaki indis (alt + ust) / 2 ile bulunur; tam sayı bölmesi kesri atar. Orta öğe hedefse indisi döner. Hedef daha küçükse sağ yarı elenir ve ust = orta - 1 olur. Hedef daha büyükse sol yarı elenir ve alt = orta + 1 olur. alt, ust'ü geçince alan boşalmıştır ve −1 döner.
| Hedef | Tur | alt | ust | orta | dizi[orta] | Karar |
|---|---|---|---|---|---|---|
| 48 | 1 | 0 | 8 | 4 | 42 | 48 > 42: alt = 5 |
| 48 | 2 | 5 | 8 | 6 | 55 | 48 < 55: ust = 5 |
| 48 | 3 | 5 | 5 | 5 | 48 | bulundu: 5 döner |
| 50 | 1 | 0 | 8 | 4 | 42 | 50 > 42: alt = 5 |
| 50 | 2 | 5 | 8 | 6 | 55 | 50 < 55: ust = 5 |
| 50 | 3 | 5 | 5 | 5 | 48 | 50 > 48: alt = 6 |
| 50 | — | 6 | 5 | — | — | 6 <= 5 yanlış: −1 döner |
Tablo 3.3.11 iki aramayı yan yana verir. 48 için orta indisler 4 (42), 6 (55) ve 5 (48)'tir. Üç karşılaştırma yeter. 50 için aynı üç adım atılır, sonra alt 6, ust 5 olur. Alan boşalmıştır ve 50'nin dizide olmadığı kesinleşir. Doğrusal arama aynı sonuca ancak dokuz karşılaştırmayla ulaşırdı.
Kazancın büyüklüğü dizi büyüdükçe görünür. n öğeli alan her adımda yarıya indiği için en çok yaklaşık log₂ n adımda tek öğeye iner. İkili aramanın zaman karmaşıklığı bu yüzden O(log n)'dir. n = 1000 için en çok 10 karşılaştırma gerekir. n = 1 000 000 için bu sayı en çok 20'dir. Doğrusal arama aynı diziler için 1000 ve 1 000 000 karşılaştırma yapabilir.
Bunun bir bedeli vardır: dizi sıralı olmalıdır. Sıralama O(n log n) iş ister ve tek bir arama için yapılmaya değmez. Aynı dizide çok sayıda arama yapılacaksa bir kez sıralayıp her aramayı O(log n)'de yapmak kazançlıdır. Arrays.binarySearch aynı yöntemi kullanır; sıralanmamış dizide sonucu anlamsızdır.
Döngü koşulunu alt < ust yazmak. Bu durumda alanda tek öğe kaldığında (alt == ust) döngü o öğeye bakmadan biter. 48 aramasının üçüncü turunda tam bu durum oluşur. Metot, 48 dizide olduğu hâlde −1 döndürür. Doğru koşul alt <= ust'tür. Aynı biçimde ust = orta yazmak da yanlıştır: alan küçülmeyebilir ve döngü sonsuza kadar döner.
3.3.5.4Eşleştirme
İki dizinin ortak öğelerini bulmak için her öğe öteki dizinin her öğesiyle karşılaştırılır. Bu iş iç içe iki döngüyle yapılır:
static void eslestir(int[] dizi1, int[] dizi2) {
for (int eleman1 : dizi1) {
for (int eleman2 : dizi2) {
if (eleman1 == eleman2) {
System.out.println("Eslesen Eleman: " + eleman1);
}
}
}
}eslestir(new int[]{3, 7, 1}, new int[]{5, 1, 3, 8}) çağrısı Eslesen Eleman: 3 ve Eslesen Eleman: 1 yazar. Çağrıda diziler değişkene atanmadan, new int[]{…} uzun biçimiyle doğrudan oluşturulmuştur. Kısa biçim {3, 7, 1} burada kullanılamaz.
Dış döngü eleman1 = 3 iken iç döngü dizi2'nin dört öğesinin hepsiyle karşılaştırma yapar. Eşleşme bulunduğunda yazdırılır, ama iç döngü durmaz; kalan öğelere de bakar. Aynı iş eleman1 = 7 ve 1 için tekrarlanır. Şekil 3.3.17 bütün karşılaştırmaları bir tablo olarak gösterir. Toplam 3 × 4 = 12 karşılaştırma yapılır.
Diziler n ve m öğeliyse n · m karşılaştırma gerekir. İki dizi de n öğeliyse bu n² olur ve eşleştirme O(n²) maliyetlidir. İç içe döngüler genellikle bu karesel büyümenin işaretidir. Daha hızlı bir yol da vardır: ikinci dizi önce sıralanır ve birinci dizinin her öğesi ikili aramayla aranır. Bu yolun maliyeti O(n log n)'dir.
3.3.5.5Karmaşıklık: O(1), O(n), O(n²)
Algoritmaların maliyetini karşılaştırmadan önce en temel işlemin, indisle erişimin maliyeti anlaşılmalıdır. dizi[2] ile dizi[1000000] aynı sürede okunur. Bunun nedeni dizinin bellekteki yerleşimidir.
int 4 bayt tutar. dizi[2]'nin adresi 1000 + 2 × 4 = 1008 olarak tek bir hesapla bulunur.Hücreler ardışık ve eşit boyutlu olduğu için i. öğenin adresi bir çarpma ve bir toplamayla hesaplanır:
adres(dizi[i]) = başlangıç + i × öğe boyutu
int için öğe boyutu 4 bayttır. Başlangıç adresi 1000 ise dizi[2] 1008'de, dizi[1000000] 4 001 000'de durur. İki hesap da aynı sürer; işlemcinin önceki öğelere bakması gerekmez. Erişimin maliyeti dizinin boyutundan bağımsızdır ve bu O(1), yani sabit zaman diye yazılır. İndisin 0'dan başlamasının bir nedeni de bu formüldür: indis 1'den başlasaydı her erişimde bir çıkarma daha gerekirdi.
| İşlem | Karmaşıklık | n = 1000 için |
|---|---|---|
Erişim dizi[i] | O(1), sabit | 1 |
| İkili arama (sıralı dizi) | O(log n), logaritmik | ≤ 10 |
| Doğrusal arama | O(n), doğrusal | ≤ 1000 |
| Tersine çevirme | O(n), doğrusal | 500 takas |
| Eşleştirme (iç içe) | O(n²), karesel | 1 000 000 |
| Seçimli sıralama | O(n²), karesel | ≈ 500 000 (tam olarak 499 500) |
Tablo 3.3.12 farkların büyüklüğünü gösterir. O(n²) algoritmalar büyük veri kümelerinde verimsizdir. n 10 kat büyürse O(n) bir algoritmanın süresi 10 kat, O(n²) bir algoritmanınki 100 kat büyür. Bir saniyede 1000 öğeyi sıralayan karesel bir algoritma, 100 000 öğe için yaklaşık üç saat sürer. O(1) erişim ise girdi boyutundan hiç etkilenmez.
3.3.5.6Seçimli sıralama
Seçimli sıralama (selection sort), her turda dizinin sıralanmamış kısmındaki en küçük öğeyi bulup bu kısmın başına taşıyan sıralama yöntemidir. Her turdan sonra sıralı kısım bir öğe büyür.
static void secimliSirala(int[] dizi) {
for (int i = 0; i < dizi.length - 1; i++) {
int minIndeks = i;
for (int j = i + 1; j < dizi.length; j++) {
if (dizi[j] < dizi[minIndeks]) {
minIndeks = j;
}
}
int gecici = dizi[minIndeks];
dizi[minIndeks] = dizi[i];
dizi[i] = gecici;
}
}i = 2 turunda sıralanmamış kısmın en küçüğü 22 bulunur ve kısmın başındaki 64 ile yer değiştirir.Dış döngü i'yi 0'dan length − 2'ye kadar sayar. i, sıralanmamış kısmın başıdır. Her turda minIndeks önce i olur; yani kısmın ilk öğesi en küçük kabul edilir. İç döngü j ile kısmın geri kalanını tarar ve daha küçük bir öğe gördükçe minIndeks'i günceller. İç döngü bitince en küçük öğe dizi[minIndeks]'tedir ve dizi[i] ile yer değiştirir.
Takas üç atamayla, geçici bir değişken yardımıyla yapılır. dizi[i] = dizi[minIndeks]; dizi[minIndeks] = dizi[i]; yazılsaydı ilk atama dizi[i]'nin eski değerini silerdi ve iki hücre de aynı değeri taşırdı. gecici eski değeri saklar. Dış döngünün length − 1 tur yapması yeterlidir: n − 1 öğe yerine oturunca son öğe kendiliğinden doğru yerdedir.
Sıralama dizinin kendisi üzerinde yapılır ve metot void'dir. Metoda dizinin referansı geldiği için takaslar çağıranın dizisini değiştirir. Bu nokta, takas işini ayrı bir metoda taşımak isteyenler için bir tuzak hazırlar.
Takası static void yerDegistir(int x, int y) { int g = x; x = y; y = g; } biçiminde yazıp yerDegistir(dizi[i], dizi[minIndeks]) çağırmak. Metoda öğelerin yalnız kopyaları gider; metot kopyaları değiştirir ve dizi aynı kalır. Çalışan sürüm dizinin kendisini ve iki indisi alır: yerDegistir(int[] d, int i, int j). Bu metot d[i] ile d[j]'yi değiştirir ve değişiklik çağıranın dizisinde görünür.
Sunumdaki örnekte {64, 25, 12, 22, 11} dizisi sıralanır. secimliSirala'ya her turun sonunda Arrays.toString(dizi) yazdıran bir satır eklenirse turlar görünür:
int[] dizi = {64, 25, 12, 22, 11};
secimliSirala(dizi);
// secimliSirala her turun sonunda yazdırır:
// System.out.println("i=" + i + ": "
// + Arrays.toString(dizi));Tur i | Taranan j | En küçük (indis) | Takas | Tur sonunda dizi |
|---|---|---|---|---|
| başlangıç | — | — | — | [64, 25, 12, 22, 11] |
| 0 | 1…4 | 11 (4) | dizi[0] ↔ dizi[4] | [11, 25, 12, 22, 64] |
| 1 | 2…4 | 12 (2) | dizi[1] ↔ dizi[2] | [11, 12, 25, 22, 64] |
| 2 | 3…4 | 22 (3) | dizi[2] ↔ dizi[3] | [11, 12, 22, 25, 64] |
| 3 | 4…4 | 25 (3) | kendisiyle | [11, 12, 22, 25, 64] |
i = 0: iç döngü en küçük değeri arar ve 11'i (indis 4) bulur. (b) Takas sonrası ilk hücre kesin yerindedir. (c) i = 1: en küçük 12 (indis 2). (d) Dört tur sonunda dizi sıralıdır.Son turda en küçük öğe zaten yerindedir ve kendisiyle takas edilir; dizi değişmez. Kod bu durumu ayrıca denetlemez. Bir if (minIndeks != i) koşulu gereksiz takası önleyebilir, ama algoritmanın maliyetini değiştirmez.
Karşılaştırma sayısı girdinin sırasından bağımsızdır. İlk turda iç döngü n − 1, ikincide n − 2 öğeye bakar; son turda tek öğeye bakar. Toplam:
(n − 1) + (n − 2) + … + 1 = n(n − 1) / 2
5 öğeli dizi için 4 + 3 + 2 + 1 = 10 karşılaştırma yapılır. n = 1000 için 499 500'dür. Dizi baştan sıralı olsa bile iç döngü her turda sonuna kadar döner; seçimli sıralamanın her durumda O(n²) olmasının nedeni budur. Takas sayısı ise en çok n − 1'dir. Takasın pahalı olduğu durumlarda bu bir üstünlüktür.
3.3.5.7Kabarcık sıralaması
Kabarcık sıralaması (bubble sort), dizinin komşu iki öğesini karşılaştırıp ters sıradaysa yer değiştiren sıralama yöntemidir. Her turda kalan kısmın en büyük öğesi sona doğru “kabarcık gibi” yükselir.
static void kabarcikSirala(int[] dizi) {
int n = dizi.length;
for (int tur = 0; tur < n - 1; tur++) {
boolean degisti = false;
for (int j = 0; j < n - 1 - tur; j++) {
if (dizi[j] > dizi[j + 1]) { // ters sıra
int gecici = dizi[j];
dizi[j] = dizi[j + 1];
dizi[j + 1] = gecici;
degisti = true;
}
}
System.out.println(Arrays.toString(dizi));
if (!degisti) break; // takas yok: sıralı
}
}{64, 25, 12, 22, 11, 36, 48} dizisi için her turun sonunda dizi şöyle olur:
[25, 12, 22, 11, 36, 48, 64]
[12, 22, 11, 25, 36, 48, 64]
[12, 11, 22, 25, 36, 48, 64]
[11, 12, 22, 25, 36, 48, 64]
[11, 12, 22, 25, 36, 48, 64]İç döngü j'yi 0'dan başlatır ve dizi[j] ile dizi[j + 1]'i karşılaştırır. Soldaki büyükse takas yapılır. İlk tur bittiğinde en büyük öğe, 64, en sağdadır; çıktının ilk satırı bunu gösterir. İkinci turda kalan altı öğenin en büyüğü, 48, sağdan ikinci hücreye yerleşir; ama 36 ile 48 zaten yerinde olduğu için bu tur onları değiştirmez.
Her turdan sonra sondaki bir öğe kesin yerine oturur. Bu yüzden iç döngünün sınırı n - 1 - tur'dur: yerine oturmuş öğelerle yeniden karşılaştırma yapılmaz. j + 1'in de dizinin içinde kalması gerekir. Sınır n - tur yazılsaydı ilk turda j = 6 olur ve dizi[7]'ye erişilmeye çalışılırdı.
degisti bayrağı algoritmanın akıllı yanıdır. Bir turda hiç takas yapılmadıysa bütün komşu çiftler doğru sıradadır, yani dizi sıralıdır. Örnekte dördüncü turdan sonra dizi sıralanmıştır. Beşinci tur hiçbir takas yapmaz ve break ile döngüden çıkılır; altıncı tura gerek kalmaz. Çıktıdaki son iki satırın aynı olmasının nedeni budur.
| Tur | Karşılaştırma | Yerine oturan | Tur sonunda dizi |
|---|---|---|---|
| 0 | 6 | 64 | [25, 12, 22, 11, 36, 48, 64] |
| 1 | 5 | 48 | [12, 22, 11, 25, 36, 48, 64] |
| 2 | 4 | 36 | [12, 11, 22, 25, 36, 48, 64] |
| 3 | 3 | 25 | [11, 12, 22, 25, 36, 48, 64] |
| 4 | 2 | takas yok: break | [11, 12, 22, 25, 36, 48, 64] |
Bu çalışmada toplam 6 + 5 + 4 + 3 + 2 = 20 karşılaştırma ve 11 takas yapılır. Dizi baştan sıralı olsaydı ilk turda hiç takas olmaz ve döngü tek turda biterdi: n − 1 karşılaştırma, yani O(n). En kötü durumda, dizi ters sıralıyken, bütün turlar çalışır ve karşılaştırma sayısı yine n(n − 1)/2 olur; O(n²). Seçimli sıralamadan farkı, takas sayısının da O(n²) olabilmesidir. Ters sıralı dizide her karşılaştırma bir takasla sonuçlanır.
{2, 1, 3, 4, 5} dizisi kabarcık sıralamasıyla kaç turda ve kaç karşılaştırmayla sıralanır?
İlk turda 2 ile 1 takas edilir, sonraki üç karşılaştırmada takas olmaz: 4 karşılaştırma, dizi [1, 2, 3, 4, 5]. degisti doğru olduğu için ikinci tura geçilir. İkinci turda 3 karşılaştırma yapılır, hiç takas olmaz ve break çalışır.
Sonuç: 2 tur, 4 + 3 = 7 karşılaştırma. Seçimli sıralama aynı dizi için 10 karşılaştırma yapardı.
3.3.5.8Diziyi tersine çevirme
Bir diziyi tersine çevirmenin kolay ama savurgan yolu, aynı boyutta ikinci bir dizi açıp öğeleri sondan başa doğru oraya kopyalamaktır. Daha iyi yol diziyi yerinde çevirmektir:
static void tersineCevir(int[] dizi) {
int baslangic = 0;
int bitis = dizi.length - 1;
while (baslangic < bitis) {
// Başlangıç ve bitiş elemanlarını değiştir
int gecici = dizi[baslangic];
dizi[baslangic] = dizi[bitis];
dizi[bitis] = gecici;
// Başlangıç indeksini artır, bitiş indeksini azalt
baslangic++;
bitis--;
}
}[1, 2, 3, 4, 5]
→ [5, 4, 3, 2, 1]
[1, 2, 3, 4]
→ [4, 3, 2, 1]baslangic soldan, bitis sağdan gelir; her turda uçtaki iki öğe takas edilir. Tek uzunlukta dizide ortadaki öğe yerinde kalır.Bu yöntemin adı iki işaretçi (two pointers) tekniğidir. baslangic 0'dan, bitis son indisten başlar. Her turda iki uçtaki öğe takas edilir, ardından işaretçiler birbirine bir adım yaklaşır. baslangic < bitis koşulu yanlış olunca, yani işaretçiler buluşunca ya da birbirini geçince, iş biter.
baslangic | bitis | Koşul | gecici | Dizi |
|---|---|---|---|---|
| 0 | 4 | 0 < 4 doğru | 1 | [5, 2, 3, 4, 1] |
| 1 | 3 | 1 < 3 doğru | 2 | [5, 4, 3, 2, 1] |
| 2 | 2 | 2 < 2 yanlış | — | döngü biter |
tersineCevir izlemesi. (a) İşaretçiler uçlarda. (b) dizi[0] ile dizi[4] takas edildi. (c) dizi[1] ile dizi[3] takas edildi. (d) baslangic = bitis = 2: döngü biter, ortadaki 3 yerinde kalır.Beş öğeli dizide iki takas yeter. Dört öğeli dizide de iki takas yapılır: önce 0 ile 3, sonra 1 ile 2; ardından baslangic 2, bitis 1 olur ve koşul yanlış çıkar. Genel olarak n/2 takas (tam sayı bölmesiyle) gerekir. Her takas üç atamadır ve iş miktarı n ile doğru orantılıdır: O(n). Ek dizi gerekmediği için ek bellek O(1)'dir. Böyle çalışan algoritmalara yerinde (in-place) algoritma denir.
Metot void'dir ve hiçbir şey döndürmez. Yine de çağıranın dizisi değişir, çünkü metoda dizinin referansı gelmiştir. Çıktıdaki → [5, 4, 3, 2, 1] satırı, main'in metot çağrısından sonra kendi dizisini yazdırmasıyla üretilir.
Tersine çevirme döngüsünü dizinin tamamı üzerinde çalıştırmak: for (int i = 0; i < d.length; i++) ile d[i] ve d[d.length - 1 - i]'yi takas etmek. İlk yarıda dizi tersine döner, ikinci yarıda aynı çiftler yeniden takas edilir ve dizi eski hâline geri gelir. {1, 2, 3, 4, 5} için sonuç yine [1, 2, 3, 4, 5]'tir. Döngü yalnız dizinin yarısını dolaşmalıdır: i < d.length / 2.
3.3.6Çok boyutlu diziler
Bazı veriler doğal olarak tablo biçimindedir: bir sınıfın derslere göre notları, bir devrenin admitans matrisi, bir resmin piksel ızgarası. Bu verileri tek boyutlu bir dizide tutmak mümkündür, ama satır ve sütun bilgisini indis hesabıyla elle yönetmek gerekir. Çok boyutlu diziler bu işi dile bırakır. Java iki ve daha fazla boyutlu dizileri destekler.
3.3.6.1İki boyutlu diziler
İki boyutlu dizi (two-dimensional array), öğeleri satır ve sütunlara yerleşen, tablo benzeri bir dizidir. Bir öğeye matris[satir][sutun] ile erişilir: ilk indis satırı, ikinci indis sütunu verir.
int[][] matris = new int[3][3]; // 3x3 boyutunda bir matris
matris[1][2] = 6; // 2. satır, 3. sütunmatris[1][2] = 6 ikinci satırın üçüncü sütununa yazar.new int[3][3] 3 satır ve 3 sütunluk bir matris oluşturur. Tek boyutlu dizilerde olduğu gibi bütün hücreler varsayılan değerle, burada 0 ile başlar. matris[1][2] = 6 satırı ikinci satırın üçüncü sütununa yazar. İndisler yine 0'dan başladığı için “2. satır” indis 1'e, “3. sütun” indis 2'ye karşılık gelir.
Satır ve sütunun sırası bir uzlaşmadır, ama tutarlı olmalıdır. Java'da ilk indis dış diziyi, ikinci indis iç diziyi seçer. Matrislerde bu, ilk indisin satır olması demektir; matematikteki aij yazımıyla da örtüşür. Resimlerde aynı nedenle [y][x] sırası kullanılır: önce satır (yükseklik), sonra sütun (genişlik).
3.3.6.2Diziler dizisi
İki boyutlu dizilere de ilklendirici listesiyle başlangıç değeri verilebilir. Bu kez her satır kendi süslü parantezi içinde yazılır:
int[][] matris = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
System.out.println(matris.length + " "
+ matris[0].length + " " + matris[1][2]);3 3 6Çıktıyı anlamak için Java'nın iki boyutlu diziyi nasıl kurduğunu bilmek gerekir. Java'da gerçek bir “tablo” yoktur. 2B dizi aslında dizilerin dizisidir. matris üç öğeli bir dizidir ve her öğesi bir int[]'e, yani bir satıra referanstır.
matris bir satır dizileri dizisidir. Dış dizinin üç hücresi, heap'teki üç ayrı satır dizisini gösterir. matris[1] ikinci satırın kendisidir.Şekil 3.3.24 bu yapıyı gösterir. matris.length dış dizinin uzunluğudur, yani satır sayısıdır: 3. matris[0] ilk satırın kendisidir ve bir int[]'tir; matris[0].length ilk satırın sütun sayısıdır: 3. matris[1][2] önce ikinci satırı seçer, sonra o satırın üçüncü öğesini okur: 6.
Bu yapının pratik sonuçları vardır. matris[1] tek başına anlamlı bir ifadedir ve bir satırı tek boyutlu dizi olarak verir. Arrays.toString(matris[1]) [4, 5, 6] yazar. Bir satır, tek boyutlu dizi bekleyen bir metoda doğrudan geçirilebilir. Buna karşılık Arrays.toString(matris) satırların içeriğini değil, kimliklerini yazar: [[I@…, [I@…, [I@…]. İç içe dizileri yazdırmak için Arrays.deepToString kullanılır.
int[][] m = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; matrisinin izi, yani köşegen öğelerinin toplamı bulunsun.
Köşegende satır ve sütun indisi eşittir: m[0][0], m[1][1], m[2][2]. Bu yüzden tek bir döngü yeter: for (int i = 0; i < m.length; i++) iz += m[i][i];.
Turlar iz'i 1, 6 ve 15 yapar. Matrisin izi 1 + 5 + 9 = 15'tir. İz yalnız kare matris için tanımlıdır; kare olmayan bir matriste m[i][i] bir satırın dışına çıkabilir.
3.3.6.3Düzensiz diziler ve Pascal üçgeni
Satırlar ayrı diziler olduğu için uzunluklarının eşit olması gerekmez. Satır uzunlukları farklı olan çok boyutlu diziye düzensiz dizi (ragged array) denir.
int[][] ucgen = new int[3][];
for (int i = 0; i < ucgen.length; i++) {
ucgen[i] = new int[i + 1];
}
System.out.println(ucgen[0].length + " " + ucgen[1].length
+ " " + ucgen[2].length);
int[][] tablo = {{1, 2}, {3, 4, 5}};
for (int[] satir : tablo) {
for (int x : satir) {
System.out.print(x + " ");
}
System.out.println();
}1 2 3
1 2
3 4 5 ucgen'in satır uzunlukları 1, 2 ve 3'tür.new int[3][] yalnız ilk boyutu verir. Üç satır referansı oluşturulur, ama satırların kendisi oluşturulmaz; üç referans da null'dır. Satırlar döngüde tek tek ayrılır: ucgen[i] = new int[i + 1]. Sonuçta satır uzunlukları 1, 2 ve 3 olur. Satır ayrılmadan ona erişilirse NullPointerException oluşur. Yalnız ikinci boyut verilemez: new int[][3] derlenmez.
Programın ikinci kısmı ilklendirici listesiyle düzensiz bir dizi kurar: tablo'nun ilk satırı iki, ikinci satırı üç öğelidir. for-each ile dolaşırken dış döngünün öğesi bir int[] satırdır; iç döngü o satırın öğelerini dolaşır. Düzensiz dizilerde iç döngünün sınırı her satır için ayrı olmalıdır. Klasik for ile yazılırken bu yüzden matris[i].length kullanılır. Sabit bir sütun sayısı yazmak kısa satırlarda sınır dışı hatasına yol açar.
Düzensiz dizinin güzel bir uygulaması Pascal üçgenidir. Üçgenin her satırı bir öncekinden bir öğe uzundur. Satırın iki ucu 1'dir; iç öğeler, üstteki satırda hemen solda ve hemen üstte duran iki öğenin toplamıdır.
int[][] p = new int[4][];
for (int i = 0; i < p.length; i++) {
p[i] = new int[i + 1];
p[i][0] = 1;
p[i][i] = 1;
for (int j = 1; j < i; j++) {
p[i][j] = p[i - 1][j - 1] + p[i - 1][j];
}
}
System.out.println(Arrays.toString(p[3]));Program [1, 3, 3, 1] yazar. Bu satır, (a + b)³ açılımının katsayılarıdır.
i | Satır oluşturma | Uçlar | İç hücreler (j = 1 … i − 1) | p[i] |
|---|---|---|---|---|
| 0 | new int[1] | p[0][0] = 1 (iki uç aynı hücre) | yok | [1] |
| 1 | new int[2] | p[1][0] = p[1][1] = 1 | yok (j < 1 hemen yanlış) | [1, 1] |
| 2 | new int[3] | 1, 1 | p[2][1] = 1 + 1 = 2 | [1, 2, 1] |
| 3 | new int[4] | 1, 1 | p[3][1] = 1 + 2 = 3, p[3][2] = 2 + 1 = 3 | [1, 3, 3, 1] |
new int[4][] yalnız dört satır referansı ayırır; hepsi null'dır. (b) Döngü bittiğinde satır uzunlukları 1, 2, 3 ve 4'tür.Kodun incelikli yeri iç döngünün sınırlarıdır. j 1'den başlar ve i'den küçük kaldıkça sürer; böylece uç hücrelere dokunulmaz. i = 0 ve i = 1 satırlarında iç hücre yoktur ve iç döngü hiç dönmez. p[i - 1][j] ifadesi de güvenlidir: j en çok i − 1 olduğu için bir önceki satırın son indisini geçmez. Satır i'nin i + 1 öğesi vardır; toplam öğe sayısı 1 + 2 + … + n = n(n + 1)/2'dir. Kare bir matris yerine düzensiz dizi kullanmak, aşağı yukarı yarı yarıya bellek kazandırır.
3.3.6.4Matrisi doldurma ve yazdırma
İki boyutlu bir diziyi dolaşmanın genel yolu iç içe iki döngüdür. Dış döngü satırları, iç döngü bir satırın sütunlarını dolaşır.
public class CokBoyutluDiziOrnegi {
public static void main(String[] args) {
// 3x3 boyutunda iki boyutlu bir dizi (matris)
int[][] matris = new int[3][3];
// Matrisin elemanlarına ilk değerlerini atama
for (int satir = 0; satir < 3; satir++) {
for (int sutun = 0; sutun < 3; sutun++) {
matris[satir][sutun] = satir * 3 + sutun + 1;
}
}
// Matrisi ekrana yazdırma
System.out.println("Matrisin Icerigi:");
for (int satir = 0; satir < 3; satir++) {
for (int sutun = 0; sutun < 3; sutun++) {
System.out.print(matris[satir][sutun] + " ");
}
System.out.println(); // Yeni satıra geçme
}
}
}Matrisin Icerigi:
1 2 3
4 5 6
7 8 9 Doldurma döngüsünde her hücreye satir * 3 + sutun + 1 yazılır. Bu ifade hücreleri satır satır, 1'den 9'a numaralar. satir * 3, önceki satırlardaki hücre sayısıdır; her satırda üç hücre vardır. sutun satır içindeki konumu ekler. + 1 ise numaralamanın 0'dan değil 1'den başlamasını sağlar.
satir | sutun = 0 | sutun = 1 | sutun = 2 |
|---|---|---|---|
| 0 | 0 + 0 + 1 = 1 | 0 + 1 + 1 = 2 | 0 + 2 + 1 = 3 |
| 1 | 3 + 0 + 1 = 4 | 3 + 1 + 1 = 5 | 3 + 2 + 1 = 6 |
| 2 | 6 + 0 + 1 = 7 | 6 + 1 + 1 = 8 | 6 + 2 + 1 = 9 |
new int[3][3]: bütün hücreler 0. (b) İlk satır doldu: 1, 2, 3. (c) İkinci satır doldu. (d) Son hücre matris[2][2] = 9.Hücreler satır satır dolar: önce [0][0], [0][1], [0][2]; sonra iç döngü biter, dış döngü bir sonraki satıra geçer. Bu sıraya satır öncelikli dolaşma denir. Yazdırma döngüsü aynı sırayla dolaşır. Her satırın öğeleri print ile yan yana yazılır; iç döngü bitince dış döngüdeki println() alt satıra geçer. println() iç döngünün içine taşınsaydı her sayı ayrı bir satıra yazılırdı.
İç içe döngülerin maliyeti satır sayısı ile sütun sayısının çarpımıdır. r × c'lik bir matrisi dolaşmak r · c adım ister. Kare n × n matris için bu O(n²)'dir. Matrisin kendisi de n² öğe taşır; her öğeye bir kez dokunmanın daha ucuz bir yolu yoktur.
int[][] m = {{1, 2, 3}, {4, 5, 6}}; matrisinin her satırının toplamı bir diziye yazılsın.
Sonuç dizisi satır sayısı kadar hücre ister: int[] t = new int[m.length];. İç içe döngüde t[i] += m[i][j]; yazılır. İç döngünün sınırı m[i].length'tir.
İlk satırın toplamı 1 + 2 + 3 = 6, ikinci satırınki 4 + 5 + 6 = 15'tir. Sonuç t = [6, 15] olur. Aynı yöntemle iki matrisin toplamı da hesaplanır: c[i][j] = a[i][j] + b[i][j]. {{1, 2}, {3, 4}} ile {{5, 6}, {7, 8}}'in toplamı [[6, 8], [10, 12]]'dir.
3.3.6.5Üç ve daha fazla boyut
Java'da üç ve daha fazla boyutlu diziler de kullanılabilir. Her ek boyut bir köşeli parantez daha demektir. Üç boyutlu dizi, iki boyutlu dizilerin dizisidir.
// 3x3x3 boyutunda üç boyutlu dizi
int[][][] ucBoyutluDizi = new int[3][3][3];
ucBoyutluDizi[1][2][0] = 16; // katman 1, satır 2, sütun 0ucBoyutluDizi[1][2][0] ikinci katmanın üçüncü satırının ilk hücresidir.Daha fazla boyut daha karmaşık verilere izin verir. Bir binanın odaları [kat][satir][sutun], renkli bir resim [y][x][kanal], zamanla değişen bir ölçüm tablosu [zaman][satir][sutun] biçiminde tutulabilir. Öğe sayısı boyutların çarpımıdır: new int[3][3][3] 3 × 3 × 3 = 27 hücre içerir. Boyut sayısı arttıkça bellek hızla büyür. 1920 × 1080'lik renkli bir resim int[1080][1920][3] olarak tutulursa 6 220 800 hücre, yaklaşık 25 MB gerekir.
int[][][] ucBoyutluDizi = new int[3][3][3];
for (int x = 0; x < 3; x++) {
for (int y = 0; y < 3; y++) {
for (int z = 0; z < 3; z++) {
ucBoyutluDizi[x][y][z] = x * 9 + y * 3 + z + 1;
}
}
}
System.out.println("Uc Boyutlu Dizinin Icerigi:");
for (int x = 0; x < 3; x++) {
for (int y = 0; y < 3; y++) {
for (int z = 0; z < 3; z++) {
System.out.print(ucBoyutluDizi[x][y][z] + " ");
}
System.out.println();
}
System.out.println();
}Uc Boyutlu Dizinin Icerigi:
1 2 3
4 5 6
7 8 9
10 11 12
13 14 15
16 17 18
19 20 21
22 23 24
25 26 27 Üç iç içe döngü diziyi dolaşır. En içteki z bir satırın sütunlarını, ortadaki y bir katmanın satırlarını, en dıştaki x katmanları dolaşır. Her hücreye x * 9 + y * 3 + z + 1 yazılır. 9, bir katmandaki hücre sayısıdır; formül, matris doldurmadaki satir * 3 + sutun + 1 kalıbının bir boyut genişletilmiş hâlidir. Sonuç olarak hücreler 1'den 27'ye numaralanır. ucBoyutluDizi[1][2][0] = 1 · 9 + 2 · 3 + 0 + 1 = 16'dır. Her katmandan sonra dış döngüdeki println() boş bir satır basar ve katmanlar çıktıda birbirinden ayrılır.
3.3.6.6Matris transpozu
Bir M matrisinin transpozu T, satırları sütuna, sütunları satıra dönüşmüş matristir: T[j][i] = M[i][j]. r × c'lik bir matrisin transpozu c × r'liktir.
public static void main(String[] args) {
int[][] matris = {
{1, 2, 3},
{4, 5, 6}
};
System.out.println("Orijinal Matris:");
yazdirMatris(matris);
int satirSayisi = matris.length;
int sutunSayisi = matris[0].length;
int[][] transpozMatris = new int[sutunSayisi][satirSayisi];
for (int i = 0; i < satirSayisi; i++) {
for (int j = 0; j < sutunSayisi; j++) {
transpozMatris[j][i] = matris[i][j];
}
}
System.out.println("Transpoz Matris:");
yazdirMatris(transpozMatris);
}Orijinal Matris:
1 2 3
4 5 6
Transpoz Matris:
1 4
2 5
3 6 Program önce boyutları diziden okur: satirSayisi = matris.length = 2, sutunSayisi = matris[0].length = 3. Transpoz için yeni bir dizi gerekir ve bu dizinin boyutları ters sıradadır: new int[sutunSayisi][satirSayisi], yani 3 × 2. Kare olmayan bir matris yerinde transpoz edilemez, çünkü sonuç farklı biçimdedir.
i | j | Okunan | Yazılan |
|---|---|---|---|
| 0 | 0 | matris[0][0] = 1 | transpozMatris[0][0] |
| 0 | 1 | matris[0][1] = 2 | transpozMatris[1][0] |
| 0 | 2 | matris[0][2] = 3 | transpozMatris[2][0] |
| 1 | 0 | matris[1][0] = 4 | transpozMatris[0][1] |
| 1 | 1 | matris[1][1] = 5 | transpozMatris[1][1] |
| 1 | 2 | matris[1][2] = 6 | transpozMatris[2][1] |
matris ve 0'larla dolu 3×2 transpozMatris. (b) matris'in ilk satırı, transpozun ilk sütunu oldu. (c) Son kopya: matris[1][2] = 6, transpozMatris[2][1]'e yazılır.Döngüler asıl matrisi satır satır dolaşır. Her öğe, transpozda indisleri yer değiştirmiş hücreye yazılır. Tablo 3.3.18 bu yer değiştirmeyi gösterir: i = 0 iken matris'in ilk satırı okunur ve transpozMatris'in ilk sütununa yazılır. Köşegendeki [1][1] hücresi yerinde kalır. İş miktarı öğe sayısıyla orantılıdır: r · c kopya.
Transpoz dizisini asıl matrisle aynı boyutta oluşturmak: new int[satirSayisi][sutunSayisi]. 2×3'lük bir matris için bu, 2×3'lük bir dizi demektir. transpozMatris[2][0]'a yazılmak istendiğinde satır indisi 2 sınır dışıdır ve ArrayIndexOutOfBoundsException oluşur. Kare matrislerde bu hata görünmez; kod ancak kare olmayan bir matrisle denendiğinde ortaya çıkar.
3.3.6.7Yardımcı metot: yazdirMatris
Transpoz programı matrisi iki kez yazdırır: önce asıl matrisi, sonra transpozu. Yazdırma kodunu iki kez yazmak yerine iş bir yardımcı metoda taşınmıştır:
public static void yazdirMatris(int[][] matris) {
for (int i = 0; i < matris.length; i++) {
for (int j = 0; j < matris[i].length; j++) {
System.out.print(matris[i][j] + " ");
}
System.out.println();
}
}Metodun parametresi int[][] matris'tir ve her boyuttaki iki boyutlu diziyle çalışır. Bunun nedeni, satır sayısının matris.length'ten, sütun sayısının matris[i].length'ten, yani diziden okunmasıdır. Aynı metot 2×3'lük asıl matrisi de 3×2'lik transpozu da doğru yazdırır. Düzensiz bir dizi verilse onu da doğru yazdırır.
Metoda dizinin kopyası değil, referansı gider. Bu yüzden büyük bir matris için bile çağrı ucuzdur: yalnız bir adres kopyalanır. Metot diziyi değiştirmediği için referansla geçirmenin bir yan etkisi de yoktur.
Sınırlar sabit yazılsaydı ne olurdu? Döngüler i < 3 ve j < 3 ile yazılmış bir yazdırma metodu 3×2'lik transpoz matrisine uygulandığında ilk satırın üçüncü öğesine erişmeye çalışır:
0 0 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2
at TranspozSabit.yazdir(TranspozSabit.java:9)
at TranspozSabit.main(TranspozSabit.java:4)Çıktıda iki 0 yazıldıktan sonra istisna oluşur. İzleme listesi iki satırlıdır: hata yazdir metodunun 9. satırındadır ve bu metot main'in 4. satırından çağrılmıştır. İzleme listesi en içteki çağrıdan dışarı doğru okunur. Sabit sayı yerine length kullanmak, metodu her boyutta doğru çalışır hâle getirir.
3.3.7Uygulama: Tic Tac Toe
Bu kesim, diziler hakkında öğrenilenleri tek bir programda birleştirir: iki boyutlu dizi, iç içe döngüler, metoda dizi geçirme, kullanıcıdan girdi ve sınır denetimi. Program, iki kişinin aynı bilgisayarda oynadığı Tic Tac Toe (X-O) oyunudur.
3.3.7.1Oyun ve veri yapısı
Oyun 3×3'lük bir tahtada oynanır. İki oyuncu, X ve O, sırayla boş bir hücreye kendi işaretini koyar. Kendi işaretinden üç tanesini aynı satıra, aynı sütuna ya da aynı çapraza yerleştiren oyunu kazanır. Boş hücre kalmazsa ve kimse kazanmamışsa oyun berabere biter.
Tahta için en doğal veri yapısı char[][] tahta = new char[3][3];'tür. Her hücre bir karakter tutar: 'X', 'O' ya da boş hücre için ' '. Bir hamle tek bir atamadır: tahta[satir][sutun] = oyuncu. Oyunun bütün durumu, tahta ve sıranın kimde olduğu bilgisinden ibarettir.
3.3.7.2main ve metotlara bölme
import java.util.Scanner;
public class TicTacToe {
static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
char[][] tahta = new char[3][3]; // 3x3'lük tahta
char oyuncu = 'X'; // İlk oyuncu X
boolean oyunDevamEdiyor = true;
tahtaDoldur(tahta); // Tüm hücreler boş
while (oyunDevamEdiyor) {
tahtayiGoster(tahta);
hamleYap(tahta, oyuncu);
oyunDevamEdiyor = oyunDevamEdiyorMu(tahta, oyuncu);
// Oyuncu değişimi
oyuncu = (oyuncu == 'X') ? 'O' : 'X';
}
tahtayiGoster(tahta);
}
// ... metotlar
}main kısa ve okunaklıdır, çünkü iş dört metoda bölünmüştür: tahtayı doldurmak, göstermek, hamle almak ve oyunun sürüp sürmediğini denetlemek. Döngü, oyunDevamEdiyor doğru oldukça bu adımları tekrarlar. Her turun sonunda sıra öteki oyuncuya geçer. (oyuncu == 'X') ? 'O' : 'X' koşul operatörüdür: oyuncu X ise O, değilse X değerini verir.
Tahta metotlara referansla gider. hamleYap tahtanın bir hücresine yazdığında değişiklik main'deki tahtada görünür, çünkü ortada tek bir dizi vardır. Bu, dizilerin referans tipi olmasının işe yarayan yüzüdür: metotlar ortak bir durumu değiştirebilir.
Scanner nesnesi static bir alan olarak sınıfın en üstünde tanımlanmıştır. Böylece bütün metotlar aynı Scanner'ı kullanır. Metotların kendisi de static'tir. main static olduğu için, bir nesne oluşturmadan çağırdığı metotların da static olması gerekir. static sözcüğü unutulursa derleyici şu iletiyi verir:
E_Static.java:4: error: non-static method tahtaDoldur(char[][]) cannot be referenced from a static context
tahtaDoldur(tahta);
^
1 error3.3.7.3Tahtayı doldurma ve gösterme
public static void tahtaDoldur(char[][] tahta) {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
tahta[i][j] = ' ';
}
}
}
public static void tahtayiGoster(char[][] tahta) {
System.out.println("-------------");
for (int i = 0; i < 3; i++) {
System.out.print("| ");
for (int j = 0; j < 3; j++) {
System.out.print(tahta[i][j] + " | ");
}
System.out.println("\n-------------");
}
}Boş tahta şöyle görünür:
-------------
| | | |
-------------
| | | |
-------------
| | | |
-------------new char[3][3] ile oluşturulan tahtanın hücreleri '\u0000' ile başlar. Bu karakter ekranda görünmez ya da bazı uçbirimlerde garip bir simge olarak çıkar. Ayrıca boş hücre denetiminde ' ' ile karşılaştırma yapılacaktır. tahtaDoldur bu yüzden bütün hücrelere önce boşluk karakteri yazar.
tahtayiGoster her satırı | X | O | | biçiminde yazar. Satırın başında "| ", her hücreden sonra " | " basılır. Satır sonundaki "\n-------------" dizgisi iki işi tek println ile yapar: \n satırı bitirir, ardından ayırıcı çizgi gelir ve println bir alt satıra geçer.
3.3.7.4Hamle alma
static Scanner scanner = new Scanner(System.in);
public static void hamleYap(char[][] tahta, char oyuncu) {
int satir, sutun;
do {
System.out.print("Sıra " + oyuncu
+ " oyuncusunda. Satır ve sütun seçin (1-3): ");
satir = scanner.nextInt() - 1;
sutun = scanner.nextInt() - 1;
} while (satir < 0 || satir > 2 || sutun < 0 || sutun > 2
|| tahta[satir][sutun] != ' ');
tahta[satir][sutun] = oyuncu;
}Kullanıcı satır ve sütunu 1–3 arasında girer, çünkü insanlar 1'den sayar. Kod - 1 ile bu değerleri 0–2 indislerine çevirir. Girdi do-while döngüsüyle alınır. do-while, gövdeyi en az bir kez çalıştırır ve koşulu sonda denetler. Soru önce sorulur; geçersiz ya da dolu bir hücre seçildiyse koşul doğru çıkar ve soru yeniden sorulur.
Koşuldaki sıralama bilinçlidir. Önce dört sınır denetimi, en son hücre denetimi yazılmıştır. || operatörü kısa devre (short-circuit) değerlendirme yapar: soldan sağa ilerler ve ilk doğru koşulda durur. Kullanıcı 4 5 girerse satir 3, sutun 4 olur. satir > 2 doğrudur ve değerlendirme orada durur. tahta[3][4] hiç okunmaz, istisna oluşmaz ve soru yeniden sorulur. Hücre denetimi sınır denetimlerinden önce yazılsaydı program ArrayIndexOutOfBoundsException ile çökerdi.
Koşulu tahta[satir][sutun] != ' ' || satir < 0 || … sırasıyla yazmak. Kısa devre yalnız soldan sağa işler; hücreye erişim en solda olduğu için sınır denetimlerinden önce yapılır. Geçerli girdilerle program sorunsuz çalışır, hata ancak kullanıcı aralık dışı bir sayı girdiğinde ortaya çıkar. Testte sınır değerleri (0, 4, −1) bu yüzden ayrıca denenmelidir.
3.3.7.5Kazanan ve beraberlik
Oyun her hamleden sonra iki soruyu cevaplar: kazanan var mı, boş hücre kaldı mı? oyunDevamEdiyorMu metodu önce kazanmayı denetler:
public static boolean oyunDevamEdiyorMu(char[][] tahta, char oyuncu) {
// Kazanan durumlarını kontrol et
if ((tahta[0][0] == oyuncu && tahta[0][1] == oyuncu && tahta[0][2] == oyuncu) ||
(tahta[1][0] == oyuncu && tahta[1][1] == oyuncu && tahta[1][2] == oyuncu) ||
(tahta[2][0] == oyuncu && tahta[2][1] == oyuncu && tahta[2][2] == oyuncu) ||
(tahta[0][0] == oyuncu && tahta[1][0] == oyuncu && tahta[2][0] == oyuncu) ||
(tahta[0][1] == oyuncu && tahta[1][1] == oyuncu && tahta[2][1] == oyuncu) ||
(tahta[0][2] == oyuncu && tahta[1][2] == oyuncu && tahta[2][2] == oyuncu) ||
(tahta[0][0] == oyuncu && tahta[1][1] == oyuncu && tahta[2][2] == oyuncu) ||
(tahta[0][2] == oyuncu && tahta[1][1] == oyuncu && tahta[2][0] == oyuncu)) {
System.out.println("Tebrikler, " + oyuncu + " oyuncusu kazandı!");
return false;
}
// ... berabere kontrolüSekiz kazanma çizgisi tek tek denetlenir. Her çizgi üç hücrenin de oyuncunun işaretini taşımasını ister; bu koşullar && ile bağlanır. Çizgilerden biri yeterlidir; çizgiler || ile bağlanır. Yalnız son hamleyi yapan oyuncu denetlenir. Yeni bir çizgiyi ancak o tamamlamış olabilir; öteki oyuncu kazanmış olsaydı oyun bir önceki hamlede biterdi.
Kazanan varsa metot false döndürür. Bu ilk bakışta ters görünür, ama metodun adı “oyun devam ediyor mu?” sorusudur ve cevap “hayır”dır. Uzun koşul ifadesi döngülerle de yazılabilirdi: üç satır ve üç sütun için bir döngü, iki çapraz için ayrı denetim. Açık yazım uzun, ama hangi çizgilerin denetlendiğini doğrudan gösterir.
Kazanan yoksa beraberlik denetlenir:
// oyunDevamEdiyorMu metodunun devamı
// Berabere durumunu kontrol et
boolean berabere = true;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (tahta[i][j] == ' ') {
berabere = false;
break;
}
}
if (!berabere) {
break;
}
}
if (berabere) {
System.out.println("Oyun berabere bitti.");
return false;
}
return true;Bu kod bir varsayımla başlar: oyun berabere bitmiştir (berabere = true). Tek bir boş hücre bu varsayımı çürütür. İç döngü boş hücre bulduğunda berabere'yi false yapar ve break ile çıkar. break yalnız içinde bulunduğu döngüden çıkar. Dış döngü çalışmayı sürdürür. Bu yüzden dış döngünün gövdesinde ikinci bir denetim gerekir: if (!berabere) break;.
Denetimlerin sırası da önemlidir. Önce kazanma, sonra beraberlik denetlenir. Son hamle tahtayı doldurup aynı anda bir çizgiyi de tamamlayabilir. Sıra ters olsaydı böyle bir oyun yanlışlıkla berabere ilan edilirdi.
İki break'in nasıl çalıştığını görmek için yalnız beraberlik kodunu, tek boş hücresi olan bir tahtayla izleyelim:
boolean berabere = true;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (tahta[i][j] == ' ') {
berabere = false;
break;
}
}
if (!berabere) {
break;
}
}
System.out.println(berabere ? "berabere" : "devam");i | j | tahta[i][j] | İşlem | berabere |
|---|---|---|---|---|
| 0 | 0 | 'X' | boş değil | true |
| 0 | 1 | 'O' | boş değil | true |
| 0 | 2 | 'X' | iç döngü kendiliğinden biter; !berabere yanlış | true |
| 1 | 0 | 'O' | boş değil | true |
| 1 | 1 | 'X' | boş değil | true |
| 1 | 2 | ' ' | boş hücre: berabere = false, iç break | false |
| 1 | — | — | !berabere doğru: dış break | false |
(1, 2) hücresinde boşluk bulundu. (b) Dış break: satır 2 hiç denetlenmez.Program devam yazar. Dokuz hücreden yalnız altısına bakılmıştır; satır 2 hiç denetlenmez. Boş hücre bulunduktan sonra aramayı sürdürmek gereksizdir, çünkü sonuç artık değişmez. Bu tür bir “ilk karşı örnekte dur” kalıbı, doğrusal aramadaki break'in iki boyutlu karşılığıdır.
3.3.7.6Örnek oyun
Aşağıdaki girdilerle bir oyun oynansın. Her çift, sırası gelen oyuncunun satır ve sütun seçimidir:
1 1 1 2 2 2 2 2 1 3 3 3| Oyuncu | Girdi | Atama | Sonuç |
|---|---|---|---|
| X | 1 1 | tahta[0][0] = 'X' | kazanan yok, boş hücre var; sıra O'ya geçer |
| O | 1 2 | tahta[0][1] = 'O' | sıra X'e geçer |
| X | 2 2 | tahta[1][1] = 'X' | sıra O'ya geçer |
| O | 2 2 | — | tahta[1][1] dolu: soru yeniden sorulur |
| O | 1 3 | tahta[0][2] = 'O' | sıra X'e geçer |
| X | 3 3 | tahta[2][2] = 'X' | ana çapraz X: kazanma iletisi, döngü biter |
do-while koşulu doğru çıkar ve soru yeniden sorulur. (c) X ana çaprazı tamamlar.Programın son çıktısı:
Tebrikler, X oyuncusu kazandı!
-------------
| X | O | O |
-------------
| | X | |
-------------
| | | X |
-------------Dördüncü hamlede O'nun seçtiği hücre doludur. tahta[1][1] != ' ' doğru çıkar ve do-while aynı soruyu bir kez daha sorar. Bu sırada sıra değişmez, çünkü hamleYap geçerli bir hamle yapılmadan dönmez. Son hamlede X ana çaprazı ([0][0], [1][1], [2][2]) tamamlar. oyunDevamEdiyorMu kazanma iletisini yazar ve false döndürür. main'deki döngü biter ve tahta son kez gösterilir.
Programın geliştirilebilecek yönleri vardır. Kullanıcı sayı yerine harf girerse nextInt() InputMismatchException fırlatır ve program durur. Beraberlik denetimi ayrı bir tahtaDoluMu metoduna taşınabilir; böylece oyunDevamEdiyorMu kısalır ve her metot tek bir soruyu cevaplar. Kazanma denetimi de son hamlenin satırına, sütununa ve gerekiyorsa çaprazlarına bakacak biçimde daraltılabilir.
3.3.8Sık yapılan hatalar
Dizilerle ilgili hatalar iki gruba ayrılır. Derleme hataları program çalışmadan önce javac'ın verdiği iletilerle görünür ve düzeltilmesi görece kolaydır. Çalışma zamanı hataları ise program çalışırken istisnayla ortaya çıkar ya da hiç ileti vermeden yanlış sonuç üretir. Aşağıdaki iki tablo, notun gövdesinde ayrı ayrı işlenen hataları bir arada verir. İletiler JDK 21'in javac ve java araçlarıyla alınmıştır.
| Hata | Kod | javac iletisi | Düzeltme |
|---|---|---|---|
| Sonradan kısa ilk değer verme | a = {1, 2, 3}; | illegal start of expression | a = new int[]{1, 2, 3}; |
| Hem boyut hem liste | new int[3]{1, 2, 3} | array creation with both dimension expression and initialization is illegal | Yalnız biri verilir. |
| Boyutu unutmak | new int[] | array dimension missing | new int[n] |
| Yanlış tipte öğe | int[] a = {1, 2.5, 3}; | incompatible types: possible lossy conversion from double to int | double[] ya da tam sayı değerler |
length() yazmak | a.length() | cannot find symbol … method length() | a.length |
| C tarzı boyut | int a[3] = {…}; | ']' expected | int[] a = {…}; |
| Dalın içinde tanımlanan değişken | if (…) { double ortanca = …; } | cannot find symbol … variable ortanca | Değişken dalların dışında tanımlanır. |
static olmayan metot | public void tahtaDoldur(…) | non-static method … cannot be referenced from a static context | public static void … |
| Hata | Sonuç | Düzeltme |
|---|---|---|
Son indisi length sanmak: a[a.length], i <= a.length | ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3 | a[a.length - 1], i < a.length |
Oluşturulmamış dizi: int[] a = null; a[0] | NullPointerException: Cannot load from int array … | Önce new ile dizi oluşturulur. |
Diziyi doğrudan yazdırmak: println(a) | [I@1c20c684 gibi bir kimlik | Arrays.toString(a) |
Atamayı kopya sanmak: b = a | b[0]'ı değiştirmek a[0]'ı da değiştirir. | a.clone(), Arrays.copyOf |
== ile içerik karşılaştırmak | Aynı öğeli iki dizi için false | Arrays.equals(a, b) |
| İlk indisi 1 sanmak | İlk öğe atlanır ya da sınır aşılır. | Kodda dizi[0]; ekranda (i + 1) + ". eleman" |
for-each ile diziye yazmak | Dizi değişmez; yalnız kopya değişir. | Klasik for ve dizi[i] = … |
| Sıralanmamış dizide ikili arama | Anlamsız sonuç, uyarı yok | Önce Arrays.sort |
| Tam sayı bölmesiyle ortalama | 29 / 6 = 4, sonra 4.0 | toplam / (double) n |
Bu hataların çoğu iki kaynağa iner. Birincisi indisin 0'dan başlaması ve son indisin length − 1 olmasıdır. İkincisi dizi değişkeninin değer değil referans tutmasıdır. Bir dizi kodu yazılırken bu iki nokta denetlenirse hataların büyük kısmı daha yazılırken önlenir. Kod denenirken de sınır durumları ayrıca sınanmalıdır: boş dizi, tek öğeli dizi, hedefin ilk ve son öğe olduğu durumlar, tek ve çift uzunluk.
3.3.9Özet
- Dizi, aynı tipten, sabit sayıda öğeyi ardışık hücrelerde tutan yapıdır. Öğelere 0'dan başlayan indisle erişilir; n öğeli dizinin son indisi n − 1'dir.
- Dizi
new int[n]ile oluşturulur ve hücreler varsayılan değer alır. Bu değer sayılar için 0 ya da 0.0,booleaniçinfalse,chariçin'\u0000', nesneler içinnull'dır. - İlklendirici listesi
{1, 2, 3}yalnız tanımlama satırında kullanılır; başka yerdenew int[]{1, 2, 3}yazılır. Boyut ile liste birlikte verilemez. lengthbir özelliktir ve parantezsiz yazılır. Dizinin bütün indislerifor (int i = 0; i < dizi.length; i++)kalıbıyla dolaşılır.for-eachdöngüsü her turda sıradaki öğenin kopyasını verir. Okumak için uygundur; diziye yazmak ve indis gerektiren işler için klasikforkullanılır.- Dizi değişkeni bir referanstır.
b = aaynı diziyi gösteren ikinci bir ad oluşturur;clone()veArrays.copyOfyeni dizi oluşturur.==adresleri,Arrays.equalsiçerikleri karşılaştırır. - Metoda dizi geçirildiğinde referansın kopyası gider. Metot hücreleri değiştirirse değişiklik çağıranda görünür; parametreye yeni dizi atamak ise çağıranı etkilemez.
- Bütün diziler heap'tedir; yığında yalnız referanslar durur. Erişilemez dizileri çöp toplayıcı temizler.
- Sınır dışı bir indis
ArrayIndexOutOfBoundsExceptiondoğurur. İleti hatalı indisi, uzunluğu ve satır numarasını verir. - Ortalama tek geçişle O(n)'de, ortanca sıralama sonrası ortadaki öğeyle, mod sıralı dizide en uzun seriyle bulunur. Ortanca ve mod sıralama yüzünden O(n log n)'dir.
- Erişim O(1), ikili arama O(log n), doğrusal arama ve tersine çevirme O(n), eşleştirme, seçimli sıralama ve kabarcık sıralaması O(n²)'dir. İkili arama yalnız sıralı dizide çalışır.
- Çok boyutlu dizi dizilerin dizisidir.
m.lengthsatır,m[i].lengtho satırın sütun sayısıdır. Satırlar farklı uzunlukta olabilir; döngülerde bu yüzdenm[i].lengthkullanılır. - Tic Tac Toe birçok tekniği birleştirir:
char[][]tahta, referansla paylaşılan durum,do-whileile girdi doğrulama, kısa devre ile sınır denetimi ve ikibreakile iç içe döngüden çıkma.
3.3.10Kavram listesi
- dizi array
- öğe element
- indis index
- ilklendirici listesi array initializer
- varsayılan değer default value
- for-each döngüsü for-each loop
- referans tipi reference type
- null null reference
- değer ile aktarım pass by value
- yığın stack (memory)
- heap heap (memory)
- çöp toplayıcı garbage collector
- dinamik dizi dynamic array
- doğrusal arama linear search
- ikili arama binary search
- zaman karmaşıklığı (O gösterimi) time complexity (Big-O)
- seçimli sıralama selection sort
- kabarcık sıralaması bubble sort
- iki boyutlu dizi two-dimensional array
- düzensiz dizi ragged array
- kısa devre değerlendirme short-circuit evaluation
3.3.11Kendini Dene
int[] d = {3, 8, 1, 9, 4};ikenfor (int i = d.length - 1; i >= 0; i -= 2) s += d[i];döngüsünden sonraskaçtır? (s0 ile başlar.)Cevap
Döngü son indisten, 4'ten başlar ve ikişer azalır:
i= 4, 2, 0. Okunan öğelerd[4]= 4,d[2]= 1 ved[0]= 3'tür.i−2 olunca koşul yanlış çıkar.s= 4 + 1 + 3 = 8. Başlangıçtad.lengthyazılsaydı ilk turdad[5]'e erişilir ve istisna oluşurdu.Şu satırlardan sonra ne yazdırılır?
int[] x = {2, 4, 6}; int[] y = x; int[] z = x.clone(); y[1] = 0; z[2] = 9;ardındanArrays.toString(x),x == yveArrays.equals(x, z).Cevap
y,xile aynı diziyi gösterir;y[1] = 0bu diziyi değiştirir vex=[2, 0, 6]olur.zayrı bir kopyadır;z[2] = 9yalnız kopyayı değiştirir vez=[2, 4, 9]olur.x == ytrue'dur.Arrays.equals(x, z)false'tur, çünkü içerikler artık farklıdır.Sıralı
{2, 5, 8, 12, 16, 23, 38, 56, 72, 91}dizisinde ikili aramayla 23 aranıyor. Hangi orta indislere bakılır? Kaç karşılaştırma yapılır?Cevap
alt= 0,ust= 9 →orta= 4 (16); 23 > 16,alt= 5.orta= (5 + 9) / 2 = 7 (56); 23 < 56,ust= 6.orta= (5 + 6) / 2 = 5 (23): bulundu. Orta indisler 4, 7 ve 5'tir. 3 karşılaştırma yapılır ve metot 5 döndürür. 40 aransaydı dördüncü adımdaorta= 6 (38) olur, ardından alan boşalır ve −1 döner.{5, 1, 4, 2, 8}dizisi seçimli sıralamayla sıralanıyor. Her turun sonunda dizi nasıldır? Kaç karşılaştırma yapılır?Cevap
i= 0: en küçük 1 (indis 1), takas:[1, 5, 4, 2, 8].i= 1: en küçük 2 (indis 3), takas:[1, 2, 4, 5, 8].i= 2: en küçük 4 zaten yerinde.i= 3: en küçük 5 zaten yerinde. Dizi ikinci turda sıralanmıştır, ama seçimli sıralama bunu fark etmez. Karşılaştırma sayısı her durumda 4 + 3 + 2 + 1 = 10'dur.int[][] m = {{1, 2}, {3, 4}, {5, 6}};içinm.length,m[0].lengthve ikinci sütunun toplamı nedir?Cevap
müç satırlı bir dizidir:m.length= 3. İlk satır iki öğelidir:m[0].length= 2. İkinci sütunun indisi 1'dir:m[0][1] + m[1][1] + m[2][1]= 2 + 4 + 6 = 12. Döngüfor (int i = 0; i < m.length; i++) t += m[i][1];biçiminde yazılır; satır değişir, sütun sabittir.int[][] r = new int[3][]; r[0] = new int[]{1}; r[2] = new int[2];satırlarından sonrar[2].length + r[1].lengthne verir?Cevap
Bir sayı vermez; program
NullPointerExceptionile durur.new int[3][]yalnız üç satır referansı oluşturur.r[0]ver[2]'ye satır atanmıştır, amar[1]hâlânull'dır.r[1].lengthbirnullreferansın alanını okumaya çalışır. İletiCannot read the array length because "…[1]" is nullbiçimindedir.static void f(int[] d) { d = new int[]{9, 9}; }vestatic void g(int[] d) { d[0] = 9; }verilsin.int[] a = {1, 2};iken öncef(a), sonrag(a)çağrılıyor. Her çağrıdan sonraanedir?Cevap
f(a)sonrasıahâlâ[1, 2]'dir.f'dekid, referansın kopyasıdır; yeni dizi atamak yalnız bu kopyayı yeni diziye bağlar.g(a)sonrasıa=[9, 2]olur.g'dekidaynı diziyi gösterir ved[0] = 9o dizinin hücresine yazar.Sıralı
{1, 1, 3, 3, 3, 5, 5, 5}dizisine notta verilen mod algoritması uygulanıyor. Sonuç nedir?>yerine>=yazılsaydı ne değişirdi?Cevap
3'ün serisi uzunluk 3'e ulaşınca 3 > 2 doğru çıkar; en uzun seri 3 × 3 olur. 5'in serisi de 3'e ulaşır, ama 3 > 3 yanlıştır. Sonuç mod 3, tekrar sayısı 3'tür; eşitlikte ilk bulunan korunur.
>=ile 5'in serisi eşit uzunlukta en uzun seriyi geçer ve sonuç 5 olurdu. Bu kümenin aslında iki modu vardır.{2, 1, 3, 4, 5}dizisidegistibayraklı kabarcık sıralamasıyla kaç turda biter? Bayrak olmasaydı kaç karşılaştırma yapılırdı?Cevap
İlk turda 2 ile 1 takas edilir (4 karşılaştırma). İkinci turda takas yapılmaz (3 karşılaştırma) ve
breakçalışır: 2 tur, 7 karşılaştırma. Bayrak olmasaydı bütün turlar çalışırdı: 4 + 3 + 2 + 1 = 10 karşılaştırma.
3.3.12İleri okuma
- Oracle, The Java Tutorials, “Arrays” ve “The for Statement” sayfaları: dizi tanımlamayı, ilklendirici listesini ve çok boyutlu dizileri anlatır.
java.util.Arraysve gelişmişforda kısa örneklerle verilir. Java'nın resmî ve ücretsiz başlangıç kaynağıdır. - A. B. Downey ve C. Mayfield, Think Java (2. baskı), Bölüm 7 “Arrays and References”: dizileri referans kavramı üzerinden anlatır. Takma ad, kopyalama ve diziyi metoda geçirme konularında bu notu tamamlar.
- P. Deitel ve H. Deitel, Java: How to Program (11. baskı), Bölüm 7 “Arrays and ArrayLists”: bol örnekli, uzun bir anlatımdır ve diziden
ArrayList'e geçişi de içerir. - J. Gosling ve diğerleri, The Java Language Specification, Java SE 21 Edition, Bölüm 10 “Arrays”: dizi tiplerinin, oluşturmanın, varsayılan değerlerin ve sınır denetiminin kesin tanımıdır. Bir ayrıntıda son söz bu belgededir.
3.3.13Kaynaklar
- Oracle, The Java Tutorials: “Arrays”; “The for Statement” (enhanced for).
- Oracle, Java SE 21 API Specification:
java.util.Arrays. - J. Gosling ve diğerleri, The Java Language Specification, Java SE 21 Edition, Oracle, 2023 (Bölüm 10: Arrays).
- P. Deitel ve H. Deitel, Java: How to Program, 11. baskı, Pearson (Bölüm 7: Arrays and ArrayLists).
- K. Sierra ve B. Bates, Head First Java, 3. baskı, O'Reilly, 2022.
- C. S. Horstmann, Core Java, Volume I: Fundamentals, 12. baskı, Pearson, 2022 (3.10 Arrays).
- A. B. Downey ve C. Mayfield, Think Java, 2. baskı, O'Reilly, 2019 (Bölüm 7: Arrays and References).
İlgili sayfalar: Bölüm 3.3 sunumu · Çalışma soruları · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.