C# İle Bilgisayar Programlama Temelleri 327 BÖLÜM Özyineleme 10 10 Bölümün İçindekileri Karışık kombinasyon problemlerini ve konfigürasyonlarını analiz ederken, permütas- yon ve türevlerini üretirken ve iç içe geçmiş döngüleri analiz ederken özyineleme ve uygulamalarını kullanacağız. Metot içinden yine metotun kendisi için yapılan çağrı veya çağrıları temsil eden güçlü programlama tekniklerine özyinelemeli denir. Bu bölümde özyinelemenin doğru ve yanlış kullanımları ile ilgili pek çok örnek göstermeye ve nasıl yararlı olabileceği hakkında doğru yolu göstermeye sizi ikna etmeliyiz. Özyineleme tekniğini kullanmak için yola çıkıyoruz. 10.2 Özyineleme Nedir? Özyineleme, belirli bir sorunu çözmek için bir yöntemin kendisini çağırdığı bir programlama tekniğidir. Metotun kendisi için yapılan bir çağrının belirli konudaki bir sorunu çözmesi olarak da tanımlanabilir. Özyineleme nesnenin kendisini içermesi yada kendi kendisi tarafından tanımlanmasıdır. Doğru kullanıldığında belirli problemlere zarif çözümler getiren bir programlama tekniğidir. Kullanımı, programlama kodunu ve okunabilirliğini bazı durumlar için kolay hale getirir. Bu programlama teknikleri genellikle kodun okunulabilirliğini artırır. 10.3 Özyineleme – Örnek Aşağıda bir Fibonacci sayı dizisinin bazı elemanlarını görüyorsunuz. 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, … www.nakov.com C# İle Bilgisayar Programlama Temelleri 328 Verilen bir Fibonacci dizisinin her elemanı iki önceki elemanın toplamından oluşur. Birinci ve ikinci elemanlar tanım gereği 1 değerini alırken, diğer elemanların diziliş kuralı aşağıda verilmiştir: F1 = F2 = 1 Fi = Fi-1 + Fi-2 (for i > 2) Tanıma göre n’inci elemanın hesaplanması için aşağıdaki özyineleme metodunu kullanırız: int 2 return 1 return 1 2 Bu örnek bir çözümün uygulanması için özyineleme metotunun nasıl kullanılacağını gösteriyor. Öte yandan, özyineleme ile programlama yaparken sezgisel olmalısınız, çünkü programın verimliği ve hızını etkileyen pek çok faktör vardır. Özyinelemenin avantaj ve dezavantajları için bkz. bu bölümün sonraki alt başlıkları. 10.4 Doğrudan ve Dolaylı Özyineleme Bir metot gövdesi içinden aynı metotun kendisine çağrı yapmışsa, bu doğrudan özyineleme veya direkt özyinelemedir. Verilen bir A metotu herhangi bir B metotunu çağırıyor ve bu B metotu da verilen bir C başka metotunu çağırıyor ve bu C metotu da gerisin geriye tekrar A metotunu çağırmışsa, bunun adı literatürde dolaylı özyineleme veya A, B ve C metotları aralarında indirekt özyineleme vardır demeklerdir. Dolaylı özyineleme çağrılarında birden fazla metot zinciri var olabilir, ancak koşullara veya verilere, parametre değerlerine bağlı olarak bir metot bir başka metota özyineleme çağrısı yaparken, yine başka nedenlerle bir başka metota özyineleme çağrısı yapabilir. www.nakov.com 329 C# İle Bilgisayar Programlama Temelleri 10.5 Özyinelemenin Sonlandırılması Özyinelemeli programlamada dikkat edilmesi gereken en önemli programlama tekniği, özyineleme zincirlerinin son bulması gerektiği, yada somut sayıda tekrar ettikten sonra sonucu döndürmesidir. Bir başka deyişle, sonsuz sayıda özyineleme yoktur, bu durum yanlış programlamaya yol açar. Bu nedenle özyinelemeyi sonlandırmak için bir yada daha fazla programlama yapısının metot içinde kullanılarak sonuç durumlarının programlanması programcının ana görevidir. Yapılması gerekli bu eylem özyinelemenin sonlandırılması olarak da tanınır. Fibonacci sayıları örneğinde, özyinelemenin sonlandırılma şartı n sayısının 2 veya daha az olmasıdır. Sonlandırılma şartının özyinelemeyi sonlandırmasından emin olmalıyız. Sonlandırılmaya gelen bir metot, daha fazla özyinelemeli çağrı yapmadan içinde bulunduğu çağrıyı sona erdirecek sonucu döndürmelidir. Kodda yapılan bir değerlendirme metotun özyinelemeli sonuç döndüren bir program mı, sonsuz döngü mü yapıldığını ortaya çıkaracaktır. Bu nedenle n=1 ve n=2 için sadece 1 döner. 10.6 Özyinelemeli Metot Oluşturulması Özyinelemeli metotun her tekrarında elde edilen sonuç toplanarak gerçek sonuç oluşturulur. Her özyinelemeli çağrı sonunda problem daha küçük alt birimlere bölünür. Bu küçük problemlere ait alt çözümler bilgisayar tarafından saklanır ve dönüş sağlandığında birleştirilir. Birleştirme işlemi için özyinelemenin sonlandırılması gerekir. 10.7 Faktoriyelin Özyinelemeli Hesaplanması Özyineleme kullanımını klasik bir örnek ile göstereceğiz – faktoriyelin özyinelemeli hesaplanması. n faktöriyel (n! olarak gösterilir) 1 ve n (dahil) arasındaki her tamsayının çarpımlarından ibaret bir sayı olarak tanımlıdır. 0! = 1 kabul edilir. n! = 1.2.3…n www.nakov.com 330 C# İle Bilgisayar Programlama Temelleri 10.7.1 Özyinelemeli Tanım Faktoriyel hesaplanması için bir çözüm oluştururken faktoriyelin özyinelemeli tanımına başvurmak yerinde olacaktır: n! = 1, n = 0 için n! = n.(n-1)!, n > 0 için 10.7.2 Özyinelemeli Bağlılık Tespiti Özyinelemeli bağlılık tespiti için uzun uğraş gerekebilir. Faktoriyel örneğimizde, problem analizi ve ilk birkaç durum için faktoriyel değerlerinin hesaplanması yeterlidir. Buradan kolayca tekrarlayan bağlılıkları görebilirsiniz: 10.7.3 Gerçekleştirilen Algoritma Özyinelemenin sonlandırılma koşulu 1 değerine sahip n=0 şartıdır. Diğer durumlarda n=n-1 için problemi çözmek zorundayız ve bu alt problem için dönen sonucun n ile çarpımını n=n-1 için döndürmeliyiz. Bu algoritma, belirli bir sayıdaki adım sonrasında özyinelemenin sonuna erişir ve 1…n arasındaki somut sayıdaki tamsayıların çarpımını hesaplar. Bu önemli şartlar ile birlikte, faktoriyel hesaplayan metotu yazabiliriz: www.nakov.com C# İle Bilgisayar Programlama Temelleri 331 static int // The bottom of the recursion 0 1 // Recursive call: the method calls itself 1 Bu metotu kullanarak konsoldan bir tamsayı okuyan, bu tamsayının faktoriyelini hesaplayan ve elde edilen değeri yazdıran bir uygulama geliştirebiliriz. RecursiveFactorial static Console int int Console “n = “ Console “{0}! = {1}” static int // The bottom of the recursion 0 return 1 // Recursive call: the method calls itself return 1 n=5 için uygulamanın çalıştırılması sonucu aşağıda verilmiştir: www.nakov.com C# İle Bilgisayar Programlama Temelleri 332 10.8 Özyineleme veya Yineleme? Özyineleme kavramı öğretilirken genellikle faktoriyel hesaplama örnek olarak seçilmiştir, ancak diğerleri gibi bu durumda da özyineleme en iyi seçim değildir. Verilen bir problem için yinelenen (ardışık) bir çözüm her zaman açık olmayabilir. Sıklıkla eğer bir problemin özyinelemeli tanımı verilmişse, özyinelemeli çözüm sezgisel ve çok zor olmamalıdır. Faktoriyel örnek için yinelemeli çözümün gerçekleştirilmesi özyineleme kadar kısa, basit ve hatta biraz daha verimli görünmektedir. int for 1 1 return Bu bölümde daha sonra özyineleme ve yineleme kullanmanın avantaj ve dezavantajlarını dikkate alacağız. Şu andan itibaren özyinelemenin gerçekleştirilmesi ile devam etmeden önce şunu da hatırlatmamız gerekir ki, yinelenen bir değişebilir şart üzerinde düşünmeli ve bu şart sonrasında daha iyi bir çözüm bulmalıyız. Özyineleme kullanılarak çözülen bir probleme göz atalım. Bu örnek için yinelemeli bir çözümü de dikkate alacağız. 10.9 İç içe N Döngünün Simülasyonu Sık sık iç içe döngüler yazmak zorundayız. İki, üç adet veya daha önceden belirli bir sayı kadar döngünün kodlanması kolaydır. Ancak sayılar önceden bilinmiyorsa, farklı bir yaklaşım düşünmekte fayda var. N sayısının 1..K defa tekrarlanarak gösterimini gerçekleştiren aşağıdaki örneği göz önüne alınız: N ve K kullanıcı tarafından girilmelidir. www.nakov.com C# İle Bilgisayar Programlama Temelleri 333 for for for 1 1 1 Console 1 “{0} {1} {2} … {N}” Örneğin N=2 ve K=3 için (1..3 defa tekrarlanarak gösterimi), ve N=3 ve K=2 için (1..2 defa tekrarlanarak gösterimi), çıktılar aşağıdaki gibidir: Bu problemi çözen algoritmayı önceki örnekte olduğu gibi açıkça anlayamayabilirsiniz. Bunun için biri özyinelemeli, diğeri yinelemeli iki çözüm geliştirebiliriz. Sonucun her satırında, N adet sayının bir sıralı dizilimini görüyorsunuz. Birinci değişken birinci döngü sayacının geçerli değerini temsil eder. İkinci değişken – ikinci döngü sayacının geçerli değerini temsil eder, vs. Her sayı 1 ve K arasında değer alabilir. N ve K verildiğinde görevimiz, N elemanın sıralı dizilimlerini bulmaktır. 10.9.1 İç içe Döngüler – Özyinelemeli Yaklaşım Eğer problem için özyinelemeli çözüm arıyorsanız, ilk yaklaşımınız özyinelemeli bağlılık bulmaktır. Örnekte tanımlı olan probleme biraz daha dikkatli bakalım. N=2 için cevabı hesaplasaydık, birinci sayı olarak K koyarak (örneğimizde K=3) N=3 için de cevabı elde edecektik, çünkü diğer sayılar N=2 için hali hazırda yazılı olacaktı. Bu sonlanma koşulu N>3 için de geçerlidir. www.nakov.com C# İle Bilgisayar Programlama Temelleri 334 Bu şekilde aşağıdaki bağımlılığı yakaladık – ilk pozisyondan itibaren geçerli her konuma 1..K arası değerleri sırasıyla yerleştirdikten sonra bir sonraki pozisyon için özyinelemeli olarak kendimizi çağırdık. Özyineleme N defa kendisini çağırdığında sonlanır ve sayılar K değerine kadar sıralı bir şekilde ard arda yazılır. C# metotu aşağıda gösterilmiştir: return for 1 1 Sayı dizisi loops adlı bir dizide saklanırken, PrintLoops() metoduyla özyineleme sonlandığında bu sayılar basılacaktır. NestedLoops(…) metotu sayıların yerleştirileceği pozisyonları belirten bir parametre almaktadır. Bir sonraki pozisyon için NestedLoops(…) metotunu özyinelemeli olarak çağırdıktan sonra döngü içinde adlandırdığımız olası her değer yazdırılır. numberOfIterations değişkeni kullanıcı tarafından girilen K değeri için oluşturulmuştur. www.nakov.com C# İle Bilgisayar Programlama Temelleri 335 Geçerli konum N olduğunda en son satır yazılmıştır ve özyineleme sonlanır. numberOfLoops değişkeni kullanıcı tarafından girilen K değeri için oluşturulmuştur. Geçerli konum N olduğunda en son satır yazılmıştır ve özyineleme sonlanır. Bu noktada tüm pozisyonlarda değerlerimiz vardır ve diziyi yazdırırız. Özyinelemeli olarak iç içe döngü ile ilgili bir çözümün tam bir uygulaması aşağıda verilmiştir: RecursiveNestedLoops static int static int static int static Console “N = “ int “K = “ Console int 0 Console int Console static return for 1 1 static for Console Console www.nakov.com 0 “{0} “ C# İle Bilgisayar Programlama Temelleri 336 N=2 ve K=4 için uygulama çıktısı aşağıdaki gibidir: 10.9 İç içe Döngüler - Yinelemeli Yaklaşım İç içe geçmiş döngülerin yinelemeli bir çözümünün uygulanması için, bir sonraki sayı dizisini bulan ve her yinelemede yazdıran aşağıdaki algoritmayı kullanabiliriz: 1. Her pozisyon başına 1 sayısını yerleştirin. 2. Mevcut sayı dizisini bastırın. 3. N pozisyon numarasını 1 artırdıktan sonra elde ettiğiniz değer K’yı aşmışsa, onu tekrar 1’e eşitleyin ve N-1 pozisyonundaki değeri 1 ile artırın. Bu değer de K’yı aşmışsa, onu tekrar 1’e eşitleyin ve N-2 pozisyonundaki değeri 1 ile artırın, ve bu böyle devam eder. 4. İlk pozisyon değeri K’yı aştığı takdirde, algoritma sonlanır. 5. Adım 2 ile devam edin. Açıkladığımız yinelemeli iç içe döngüler algoritmasının uygulaması aşağıda sunulmuştur: www.nakov.com C# İle Bilgisayar Programlama Temelleri 337 using class IterativeNestedLoops int void Console “N = “ int “K = “ Console int Console int Console void int while true 1 1 1 if 0 return 1 void int 0 1 www.nakov.com C# İle Bilgisayar Programlama Temelleri 338 static int Console 0 0 Console Main() ve PrintLoops() metotları özyinelemeli uygulamadaki gibidir. NestedLoops() metotu yinelemeli ve parametresiz olarak çalıştığı için farklı uygulanmalıdır. Metotun en başında, InitLoops() metotuna bir çağrı yapılarak dizinin her pozisyonu için 1 atanır. Algoritmanın adımları çalışırken sonsuz döngüden metotların return deyimi ile çıkar. Algoritmanın 3.adımını uygulamak oldukça ilginçtir. K’nın üzerindeki değerlerin kontrolü, 1’e eşitlenmeleri, ve bir önceki pozisyonun değerinin 1 ile artırılması (aynı kontrolü onun için de gerçekleştirdikten sonra) ardından K’yı aşan değerler için while döngüsüne girilir. Mevcut pozisyon değeri bu amaçla 1’e eşitlenir. Bunun sonrasında bir önceki pozisyon geçerli olur. Ardından geçerli pozisyonun değeri 1’e eşitlenir ve döngünün başlangıcına dönülür. Bu eylemler mevcut pozisyon değeri K veya daha az iken devam eder. K’yı aşınca (numberOfIterations değişkeni K değerine erişince) sona erer. İlk pozisyon değeri K’yı aşınca, döngü K defa çalışmıştır. Çıktının devamlılığını sağlayabilmek için (sonsuz döngüye devam etmek için) sayı dizisinin bir sonraki elemanı tekrar 1’e eşitlenir ve bu anda bu alt döngünün yinelenme sayısı K’yı tükettiği için geçerli pozisyon (currentPosition) 0’dan az ve negatif bir değer almıştır. Dizi endeksi negatif değer alamayacağı için bir if-kontrol deyimi içindeki return ile alt döngüden çıkılır. N=3 ve K=2 için uygulamayı test edebiliriz: www.nakov.com C# İle Bilgisayar Programlama Temelleri 339 10.9.3 Hangisi Daha İyi: Özyineleme veya Yineleme? Eğer problem çözme algoritması özyinelemeli ise, özyinelemeli çözüm uygulaması aynı problem için geliştirilebilecek bir yinelemeli çözüm uygulamasından çok daha okunaklıdır ve tercih edilir. Eşdeğer bir algoritmanın tanımlanması ve algoritmaların eşdeğerliliklerini kanıtlamak bazen zor olabilir. Özyineleme kullanarak belirli durumlarda çok daha basit, daha kısa ve kolay çözümler sunmayı başarabiliriz. Öte yandan, özyinelemeli çağrılar çok daha fazla kaynak tüketebilir (CPU zamanı ve bellek). Her özyinelemeli çağrı için ayrıca yığında, metota giren ve/veya çıkan argümanlar, yerel değişkenler ve döndürülen değerleri saklayan yeni bellek alanları ayrılmalıdır. Çok fazla özyinelemeli çağrılar bir yan etki olarak bellek akıntısına neden olabilir. Özyinelemeli çözümler, bazı durumlarda, eşdeğer yinelemeli çözümlerinden daha zor okunabilir ve anlaşılabilir. Özyineleme güçlü bir programlama tekniğidir, ancak dikkatli analiz edilmelidir. Yanlış kullanımında, çözümlerin anlaşılması ve bakımı zorlaşır. ! Eğer özyineleme kullanarak daha basit, daha kısa ve daha kolay bir çözüme varırsanız tercih edilebilir. Aksi takdirde, yinelemeli bir çözüm düşününüz. www.nakov.com C# İle Bilgisayar Programlama Temelleri 340 Aşağıda özyinelemenin doğru kullanımını gösteren bir uygulama verilmiştir: RecursiveFibonacciMemoization static long static Console int int long Console static 1 2 1 1 “n = “ Console long 2 “fib({0}) = {1}” int 1 return www.nakov.com 2 341 C# İle Bilgisayar Programlama Temelleri Main() ve PrintLoops() metotları özyinelemeli uygulamadaki gibidir. NestedLoops() metotu yinelemeli ve parametresiz olarak çalıştığı için farklı uygulanmalıdır. Metotun en başında, InitLoops() metotuna bir çağrı yapılarak dizinin her pozisyonu için 1 atanır. Algoritmanın adımları çalışırken sonsuz döngüden metotların return deyimi ile çıkar. Algoritmanın 3.adımını uygulamak oldukça ilginçtir. K’nın üzerindeki değerlerin kontrolü, 1’e eşitlenmeleri, ve bir önceki pozisyonun değerinin 1 ile artırılması (aynı kontrolü onun için de gerçekleştirdikten sonra) ardından K’yı aşan değerler için while döngüsüne girilir. Mevcut pozisyon değeri bu amaçla 1’e eşitlenir. Bunun sonrasında bir önceki pozisyon geçerli olur. Ardından geçerli pozisyonun değeri 1’e eşitlenir ve döngünün başlangıcına dönülür. Bu eylemler mevcut pozisyon değeri K veya daha az iken devam eder. K’yı aşınca (numberOfIterations değişkeni K değerine erişince) sona erer. İlk pozisyon değeri K’yı aşınca, döngü K defa çalışmıştır. Çıktının devamlılığını sağlayabilmek için (sonsuz döngüye devam etmek için) sayı dizisinin bir sonraki elemanı tekrar 1’e eşitlenir ve bu anda bu alt döngünün yinelenme sayısı K’yı tükettiği için geçerli pozisyon (currentPosition) 0’dan az ve negatif bir değer almıştır. Dizi endeksi negatif değer alamayacağı için bir if-kontrol deyimi içindeki return ile alt döngüden çıkılır. N=3 ve K=2 için uygulamayı test edebiliriz: Gözle görülür bir fark vardır. Önceki örnekte n=100 için sonsuza giden bir hesaplama nedeniyle sonuç uzun zaman sonra hesaplanırken, optimize edilmiş çözüm ile sonuç hemen hesaplanıyor. Daha sonraki “Veri Yapıları ve Algoritma Karmaşıklığı” Bölümü’nde not edileceği üzere, birinci çözüm üssel, ikinci çözüm doğrusal zamanda çalışmaktadır. www.nakov.com C# İle Bilgisayar Programlama Temelleri 342 10.9.4 Fibonacci Sayıları – Özyinelemenin Yanlış Kullanımı Bir Fibonacci dizisinin n.elemanını bulduğumuz örneğe yeniden göz atalım ve özyinelemeli çözüme daha dikkatli olarak yaklaşalım. int 2 return 1 return 1 2 Bu çözüm, sezgisel, kısa ve anlaşılırlığı yüksektir. İlk bakışta özyinelemenin iyi uygulanabileceği bir örnekmiş gibi görünüyor. Ancak gerçekte, özyinelemenin kullanımına dair doğru olmayacak bir örnektir. Nedenini açıklamak gerekirse, özyinelemeli uygulamayı aşağıdaki şekilde geliştirdiğimizi düşünelim: RecursiveFibonacci static Console int int long Console “n = “ Console “fib({0}) = {1}” static int 2 return 1 return www.nakov.com 1 2 343 C# İle Bilgisayar Programlama Temelleri n=100 için, hesaplamalar kimsenin beklemeyi göze almayacağı kadar uzun süreceği için uygulama son derece verimsiz olacaktır. Özyinelemeli her çağrı için iki özyinelemeli çağrı daha yapılıyor. Çağrılar, bu nedenle, aşağıdaki şekilde gösterildiği gibi katlanarak büyümektedir. fib(100) hesaplanması için gerekli adımlar 100 üssü 1,6 mertebesindedir, matematiksel olarak bu sayı kanıtlanmıştır. Buna rağmen, çözüm doğrusal olsaydı, bu sayı 100 olacaktı. Nedeni, çok fazla hesap gerektirmesindendir. Aşağıdaki Fibonacci ağacından da görülebileceği gibi, fib(2) sayısı birden fazla yerde göze çarpmaktadır. 10.9.5 Fibonacci Sayıları – Özyinelemenin Doğru Kullanımı Fibonacci sayılarının hesaplanması için özyinelemeli metotu optimize etmenize yardımcı olabiliriz. Dizide zaten hesaplanan sayıları hatırlayarak (saklayarak) ve ancak eğer sayı henüz hesaplanmamışsa özyinelemeli çağrıyı yapmaya hazırlanmakla optimizasyon sağlanabilir. Bilgisayar bilimlerinde dinamik optimizasyon veya memoizasyon olarak bilinen bu optimizasyon tekniği sayesinde (hafıza teknikleri ile karıştırmayınız), özyinelemeli çözüm adımları doğrusal bir sayı kadar yinelendikten sonra sona erer. www.nakov.com C# İle Bilgisayar Programlama Temelleri 344 Aşağıda özyinelemenin doğru kullanımını gösteren bir uygulama verilmiştir: RecursiveFibonacciMemoization static long static Console int int long Console static 1 2 1 1 “n = “ Console long 2 “fib({0}) = {1}” int 1 2 return Gözle görülür bir fark vardır. Önceki örnekte n=100 için sonsuza giden bir hesaplama nedeniyle sonuç uzun zaman sonra hesaplanırken, optimize edilmiş çözüm ile sonuç hemen hesaplanıyor. Daha sonraki “Veri Yapıları ve Algoritma Karmaşıklığı” Bölümü’nde not edileceği üzere, birinci çözüm üssel, ikinci çözüm doğrusal zamanda çalışmaktadır. www.nakov.com C# İle Bilgisayar Programlama Temelleri 345 10.9.6 Fibonacci Sayıları – Yinelemeli Çözüm Fibonacci sayılarının ardışık hesaplanmasıyla özyineleme kullanmadan problemi çözmenin de mümkün olabileceğini fark etmişsinizdir. Bu amaçla dizinin sadece son iki elemanı saklanır ve bir sonraki elemanın değerini hesaplamak için saklanan değerler kullanılır. Fibonacci sayılarını yinelemeli hesaplayan algoritmanın bir uygulaması aşağıda verilmiştir: IterativeFibonacci static Console int int long Console “n = “ Console “fib({0}) = {1}” static long long long for int 0 2 1 1 return Bu çözüm kısa ve zarif görünüyor, verimlidir ve fazla bellek gerektirmez, ancak yine de özyineleme için varolan riskleri tamamen yok etmiyor. Bir başka öneriyle önceki örnekleri sonuçlandıralım: ! Özyineleme güçlü bir programlama tekniğidir, fakat çalışması ve etki alanı hakkında yeterli bilgiye sahip olunmadığı sürece ne olduğunu fark edemeyebilir ve kendi bindiğiniz dalı kesebilirsiniz. Bu da yanlış programlamaya neden olacağı için dikkatle kullanmalısınız! www.nakov.com 346 C# İle Bilgisayar Programlama Temelleri Eğer bu kuralı uygularsanız, özyinelemenin yanlış kullanım olasılığı azalır ve sonuçlarından etkilenmezsiniz. 10.9.7 Özyineleme ve Yineleme – Daha Fazlası Doğrusal hesaplama gerektiren süreçler için özyineleme kullanmak genellikle zorunlu değildir, çünkü yinelemeyle sonuçlar kolayca bir araya getirilebilir, bu basit ve etkili bir hesaplama tekniğidir. Örneğin, doğrusal işlem gerektiren faktöriyel hesaplama probleminde, bir sonraki elemanın değeri sadece önceki gelen tüm elemanlara bağlıdır. Doğrusal hesaplama işlemlerinin karakteristik özelliği, her adımda sadece bir kere özyineleme çağrısı ile hesabın sadece bir yönde ilerlenerek tamamlanabilmesidir. Doğrusal hesaplama sürecini şematik olarak şu şekilde tanımlayabiliriz: void Metot gövdesinde sadece bir kere özyinelemeli çağrı yapılmışsa özyineleme kullanmak gerekli değildir, çünkü yineleme anlaşılır ve barizdir. Bununla birlikte, bazen, dallanmış bir hesaplama süreci (ağaç gibi) ile yüz yüze kalırız. Örneğin, N iç içe geçmiş döngülerin taklit edilmesi, yinelemeyle kolayca ifade edilemez. Muhtemelen, iç içe geçmiş döngüleri taklit eden yinelemeli algoritmamızın tamamen farklı bir prensipte çalıştığını fark etmişsinizdir. Aynı şeyi yineleme olmadan uygulamayı deneyin ve kolay olmadığını göreceksiniz. Normalde, her özyineleme, (programın yürütülmesiyle oluşturulan) çağrı yığınını kullanarak yinelemeye dönüşebilir, ancak bu karmaşıktır ve bunu yapmanın hiçbir yararı yoktur. Özyineleme, belirgin bir yinelemeli çözüme sahip olmadığımız bir problem için basit, anlaşılması kolay ve etkili bir çözüm sağlaması durumunda kullanılmalıdır. Yinelemenin her adımında ağaç benzeri (dallı) hesaplama süreçlerinde birkaç özyinelemeli çağrı yapılır ve hesaplamanın şeması ağaç olarak görülebilir (doğrusal hesaplamalarda olduğu gibi bir liste değil). Örneğin, Fibonacci sayılarını hesapladığımızda özyinelemeli çağrı ağacının nasıl olacağını gördük. www.nakov.com C# İle Bilgisayar Programlama Temelleri 347 Ağaç benzeri dallara sahip bir hesaplama sürecinin şematik gösterimi aşağıdaki gibidir: void Ağacın hesaplama süreçleri (doğrusal süreçlerin aksine) doğrudan özyinelemeli olarak ifade edilemez. Fibonacci için durum basittir, Çünkü her bir sonraki sayı önceki sayıya bakarak hesaplanabilir. Bazen, her sonraki sayı, yalnızca önceki yoluyla hesaplanmaz, bir sonraki sayı da gereklidir, ve özyinelemeli bağımlılık o kadar basit değildir. Bu durumda memoizasyon tekniğini uygulayarak yinelenen hesaplamalardan kaçındığınız takdirde, özyineleme doğru olarak uygulanırsa, çok verimli sonuç verir. ! Dallı özyinelemeli hesaplamalar için özyineleme kullanın (ve her bir değerin yalnızca bir kez hesaplandığından emin olun). Doğrusal özyinelemeli hesaplamalar için özyinelemeyi tercih edin. Son ifadeyi klasik bir örnekle göstereceğiz. 10.9.8 Labirentte İz Sürmek – Örnek N x M kapıdan oluşan dikdörtgen şekilli bir labirent veriliyor. Kapıların her biri geçilir veya geçilmez özelliklidir. Sol üst köşesinden labirente giren bir kimse, sağ alt köşeye ulaşmak istiyor. Kişi yukarı, aşağı, sağa veya sola yöne bir seferde bir kapı hareket edebilir. Bir kapıdan geçildiğinde, geri dönüş yapılmaksızın tekrar aynı kapıdan geçilmesi yasaktır. Ancak kapı serbest hale gelebilirse geçilebilir. Sağ alt köşeye ulaşıldığına yol tamamlanıyor. Labirente giren bir kişinin girişten çıkışa kadar izlediği tüm yolları görüntülemek istiyoruz. www.nakov.com 348 C# İle Bilgisayar Programlama Temelleri Özyineleme kullanılarak çözülebilen problemlere tipik bir örnektir. Bu problemin yineleme ile çözümü daha karmaşık ve uygulaması zor olacaktır. Şekiller üzerinden problemi anlamaya çalışınız: Belirtimleri yerine getiren girişten çıkışa 3 ayrı yol olduğunu görebilirsiniz. (sadece geçerli kapılardan geçilmiştir ve aynı kapıdan iki kere geçilmemiştir). Bu 3 yol aşağıda izlenmiştir: Yukarıdaki şekillerde sayılar yolu izlerken kaç defa dönüş yapıldığına işaret ediyor. www.nakov.com C# İle Bilgisayar Programlama Temelleri 349 10.9.9 Labirent Yolları – Özyinelemeli Algoritma Bu problemi nasıl çözeriz? Labirentin herhangi bir pozisyonundan sonuna kadar arama işlemini bir özyinelemeli süreç olarak aşağıda gibi tanımlayabiliriz: 1. Başlangıç pozisyonu (0,0) olmak üzere, labirentin geçerli konumu (row, col) olsun. 2. Mevcut konum (N-1, M-1) ise hedefe varılmıştır. Aranan çıkış sağlanmıştır. 3. Kapı geçilmezse, geri gideriz (durma hakkımız yoktur). 4. Pozisyon daha önce ziyaret edilmişse, geri gideriz (iki kere geçme hakkımız yoktur). 5. Bunların dışında, 4 özyineleme sözkonusudur: • Sağa gidiş: (row, col + 1) • Aşağı gidiş: (row + 1, col) • Yukarı gidiş: (row – 1, col) • Sola gidiş: (row, col - 1) Bu algoritma çözümünü analiz ederken özyinelemeli düşünmeliyiz. Problem cümlesi “verilen bir pozisyondan çıkışa giden yolu bulmaktır”. Alt problemlere bölünmesi gerekirse: • geçerli konumun bir sağındaki pozisyondan çıkışa giden yolu aramak • geçerli konumun bir aşağısındaki pozisyondan çıkışa giden yolu aramak • geçerli konumun bir yukarısındaki pozisyondan çıkışa giden yolu aramak • geçerli konumun bir solundaki pozisyondan çıkışa giden yolu aramak Ulaştığımız her olası konumdan dört muhtemel yönü kontrol edersek ve bir daire içerisinde hareket etmezsek (daha önce üzerine adım attığımız konumlardan geçmekten kaçınarak) er yada geç çıkış yolunu bulmalıyız (varsa). Bu sefer özyineleme önceki problemler kadar basit değildir. Her adımda çıkışa ulaşıp ulaşmadığımızı ve yasak bir konumda olup olmadığımızı kontrol etmeliyiz; Bundan sonra, bulunduğumuz pozisyonu ziyaret edilmiş olarak işaretlemeliyiz ve dört yönde aramayı özyinelemeli olarak çağırmalıyız. Özyinelemeli aramalardan döndükten sonra, başlangıç noktasını ziyaret edilmemiş olarak işaretlemeliyiz. Bu gibi gezinme, geriye doğru arama (backtracking) olarak bilinir. www.nakov.com C# İle Bilgisayar Programlama Temelleri 350 10.9.10 Labirent Yolları – Uygulama Algoritma uygulaması için uygun bir şekilde labirenti temsil etmeliyiz. İki boyutlu bir karakter dizisi kullanacağız. Geçilir pozisyonlar ‘ ‘ boşluk karakteri ile, çıkış pozisyonu ‘e ‘ ile, geçilmez pozisyonlar ‘*’ yıldız karakteri ile gösterilecek. Başlangıç pozisyonu geçilir pozisyondur. Ziyaret ettiğimiz pozisyonları ‘s’ ile işaretleyeceğiz. Bir labirent temsili aşağıda verilmiştir: char ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘*’ ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘*’ ‘ ‘ ‘*’ ‘*’ ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘*’ ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘e’ Labirent yollarının bulunması için özyinelemeli metotun uygulaması şöyle olacaktır: www.nakov.com C# İle Bilgisayar Programlama Temelleri 351 char ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘*’ ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘*’ ‘ ‘ ‘*’ ‘*’ ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘*’ ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘e’ 0 0 1 // We are out of the labyrinth return // Check if we have found the exit ‘e’ ‘ ‘ // Mark the current cell as visited ‘s’ // Invoke recursion to explore all possible directions 1 // left 1 // up 1 // right 1 // down // Mark back the current cell as free ‘ ‘ 0 www.nakov.com 0 352 C# İle Bilgisayar Programlama Temelleri Uygulama yukarıdaki tanımı aynen gerçekleştirir. Bu durumda labirentin boyutları N ve M değişkenlerinde saklanmaz, ancak iki-boyutlu bir lab dizisi yardımıyla, labirentin sütun sayısı lab.GetLength(1) ve satır sayısı lab.GetLength(0) olmak üzere elde edilir. Arama yapan özyinelemeli metotun girişinde ilk koşul labirentin dışına çıkıp çıkmadığımızın kontrolüdür. Bu yasak durumda, labirentin sınırları dışına çıkılmaması için metot sonlandırıldı. Bundan sonra, çıkışı bulup bulmadığımızı kontrol etmeliyiz. Bulduysak, uygun bir mesaj yazdırıyoruz ve bulunduğunuz konumdan ileriye doğru aramayı sonlandırıyoruz. Sonra, mevcut karenin kullanılabilir olup olmadığını kontrol ediyoruz. Eğer pozisyon geçerliyse, ve önceki adımların bazılarında bu kareye basmadıysak (başlangıç pozisyonundan labirentin mevcut hücresine kadar olan mevcut yolun bir parçası değilse), mevcut kare kullanılabilir. Eğer hücre mevcutsa, üzerine basarız. Tanım gereği bu hücreyi ‘s’ karakteriyle işaretleyerek ziyaret edilmiş olarak kaydetmeliyiz. Bundan sonra dört olası yönde özyinelemeli olarak bir yol ararız. Bu dört özyinelemeli arama çağrısından geri dönüldüğünde, geçerli hücreden geri adım atarak, mevcut hücre ‘ ‘ ile işaretlenerek serbest bırakılır. Geçerli konumun serbest bırakılması önemlidir, çünkü geri döndüğümüzde mevcut yolun bir parçası değildir. Bu belirtimi atlarsanız, yolların hepsi değil, ancak bazıları bulunmuş olabilir. Labirentin çıkış yollarını bulmak için uygulanan özyinelemeli metot bu şekilde özetlenmiştir. Şimdi sadece bu metotu (0, 0) başlangıç pozisyonundan başlatarak Main() metotu içinden çağırmalıyız. Program yürütüldüğünde aşağıdaki sonucu döndürür: Çıkışın tam üç kez bulunduğunu görebilirsiniz. Algoritma düzgün çalışıyor gibi görünüyor; ancak daha anlamlı olabilmesi için çıkış yolunu yazdırmamız gerekiyor. www.nakov.com 353 C# İle Bilgisayar Programlama Temelleri 10.9.11 Labirent Yolları – İz Kayıtları Özyinelemeli algoritma ile bulduğumuz yolları yazdırmak için, her adımda atılan adım yönünü saklayacak bir diziye ihtiyacımız var: sağ – R (right), aşağı – D (down), yukarı – U (up), sol – L (left). Bu dizi her an için labirentin başından itibaren geçerli yolu tutacak. Bir karakter dizisi ve sayaç yardımıyla, şu anki özyineleme derinliğini, yani özyinelemeli olarak kaç pozisyon ilerlediğimizi tutabiliriz. Özyinelemeye her girişte sayaç 1 artırılmalıdır. Dönüşte 1 azaltılmalıdır. Çıkış yolu bulunduğunda, yazdırılması için gerekli veriler hazırdır (0 ile başlayan ve sayaç endeksine kadar olan dizinin tüm karakterleri). Dizinin boyutları ne olmalıdır? Bu sorunun cevabı kolaydır; bir hücreye en fazla bir kere girilebildiğine göre en uzun yol N * M kadardır. Örneğimizdeki labirent 7 * 5 boyutlarında olduğu için dizinin en fazla uzunluğu 35 kabul edilmelidir. Not: List<T> veri yapısını biliyorsanız, karakter dizisi yerine List<char> kullanmanız daha uygun olabilir. Listeleri daha detaylı olarak “Doğrusal Veri Yapıları” Bölümü’nde öğreneceksiniz. Açıklanan fikrin uygulanması aşağıda verilmiştir: www.nakov.com C# İle Bilgisayar Programlama Temelleri 354 char ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘*’ ‘ ‘ ‘*’ ‘ ‘ 0 ‘ ‘ ‘ ‘ ‘ ‘ ‘*’ ‘ ‘ ‘*’ ‘*’ ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘*’ ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘e’ 0 1 // We are out of the labyrinth return // Append the direction to the path // Check if we have found the exit ‘e’ ‘ ‘ // The current cell is free. Mark it as visited ‘s’ // Invoke recursion to explore all possible directions 1, ‘L’ // left ‘U’ // up 1, ‘R’ // right 1 ‘D’ // down // Mark back the current cell as free ‘ ‘ 1 // Remove the last direction from the path www.nakov.com C# İle Bilgisayar Programlama Temelleri 355 static void char[] Console int Found path to the exit: Console Console. void ‘S’ Labirentin çıkış yolunu arayan özyinelemeli metota bir parametre daha ekledik: geçerli konuma ulaşmak için kullandığımız yönü belirtir imleç : R, D, U, L. Bu parametre başlangıç pozisyonundan geçerken S değerine sahiptir. Ancak bu bir anlam taşımaz. Yolu yazdırırken, bu ilk elemanı atlarız. Programı başlatırsak, labirentin başından sonuna kadar üç yol bulunur: 10.9.12 Labirent Yolları – Programın Test Edilmesi Algoritma düzgün çalışıyor gibi görünüyor. Biraz daha fazla örnek ile test etmek hatasız çalıştığını gösterecektir. 1x1 boş labirent ile programı test edebilirsiniz, 3 x 3 boş labirent ile, yada çıkış yolu olmayan bir labirent ile ve birçok yola sahip boyutları büyük bir labirent ile test edebilirsiniz. Bu testler her durumda programın düzgün çalıştığına sizi ikna edecektir. www.nakov.com C# İle Bilgisayar Programlama Temelleri 356 Örnek girdi (1 x 1 boş labirenti): char ‘e’ Örnek çıktı: Çıktıdaki yol beklendiği gibi boştur (uzunluğu 0’dır), çünkü başlangıç pozisyonu çıkışla aynıdır. Bu durumda görselleştirmeyi geliştirmek isteyebilirsiniz (“Yol boştur” yazdırmak, vs.) Örnek girdi ( 3 x 3 boş labirenti): char ‘ ‘ ‘ ‘ ‘ ‘ www.nakov.com ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘e’ C# İle Bilgisayar Programlama Temelleri 357 Yukarıdaki örnek labirentin çıktısı: Çıktının doğru olduğunu kabul edebilirsiniz - bunlar tüm çıkış yollarıdır. Bir başka örnek girdi (çıkışı olmayan 5x3 labirenti): char ‘ ‘ ‘*’ ‘*’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘*’ ‘ ‘ ‘*’ ‘ ‘ ‘ ‘ ‘*’ ‘e’ Çıktısı: (çıkış yok) Çıktının doğru olduğunu görebilirsiniz, ancak yine de herhangi bir çıktı yerine daha anlamlı bir mesaj ekleyebilirsiniz (örneğin, “Çıkış yok!”). www.nakov.com C# İle Bilgisayar Programlama Temelleri 358 Şimdi çok büyük bir labirent için programa bakalım: Örnek girdi (15x9 labirenti): char Program çalıştırıldığında çıkış yollarını yazdırmaya başlar, ancak sonlanmayacaktır çünkü çıkış yolları oldukça fazladır. Birkaç yol fazlası aşağıda verilmiştir: www.nakov.com C# İle Bilgisayar Programlama Temelleri 359 Şimdi, son bir örnek için programı çalıştıralım: Örnek girdi (çıkışı olmayan 15 x 9 labirenti): char ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ’*’ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’*’ ’ ‘ ’ ‘ ’ ‘ ’*’ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’*’ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’*’ ’ ‘ ’ ‘ ’ ’ ’ ’ ’ ’ ’ ’ ’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’*’ ’*’ ’*’ ’*’ ’*’ ’*’ ’ ‘ ’ ‘ ’*’ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ’ ’ ’ ’ ’ ’ ’ ’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ’ ’ ’ ’ ’ ’ ’ ’ ’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ’ ’ ’ ’ ’ ’ ’ ’ ’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ’ ’ ’ ’ ’ ’ ’ ’ ’ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ‘ ’*’ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’*’ ’*’ ’ ‘ ’*’ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’*’ ’*’ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’*’ ’ ‘ ’*’ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’ ‘ ’*’ ’ ‘ ’e’ Program çalıştırıldığında çıktı yazdırmadan kilitleniyor. Çok uzun zamandır çalıştığı için sorun olabilir. Sorun nedir? Ortalama boyutu 20 olan bir labirent için 4 kere özyinelemeli çağrı vardır. Bunun anlamı 420 kere özyineleme çağrılır. Bu da programın aramasını tamamlayıp sonlanması için oldukça büyük bir rakamdır. Belki sayılar tam kesinlikte değildir, ancak programın çalışma zamanı hakkında bize bir fikir veriyor. Sonuç nedir? Değişkenler çok fazla olduğunda geriye doğru izleme yöntemi işe yaramaz. Bu sorun büyük bir labirentin tüm çıkış yollarını bulma problemi için geçerlidir. Çalışma zamanı sorununu çözmek için daha fazla enerji tüketmenizi istemeyeceğiz. Ancak şunu belirtmekte fayda vardır ki, problem tanımımızı çok az değiştirerek, yani örneğin sadece bir yol bulunmasını isteyerek, çalışma zamanı sorunundan sizi kurtarabiliriz. Programda geri dönülen yollar için özyinelemenin mevcut geçerli pozisyonunu serbest bırakmayarak bu değişikliği gerçekleştirebiliriz. Bu, aşağıdaki satırları koddan silmek demektir: // Mark back the current cell as free ‘ ‘ Bunun sonrasında program çok hızlı olarak çıkış olup olmadığına karar verir, ve varsa onu çabucak bulur. En kısa veya en uzun değil, sadece ilk yolu muhakkak bulacaktır. www.nakov.com 360 C# İle Bilgisayar Programlama Temelleri 10.10 Özyineleme Kullanımı – Sonuçlar Labirentte çıkış yolu arama probleminden genel sonuç zaten formüle edilmiştir: Eğer özyinelemenin nasıl çalıştığını çok iyi anlamakta güçlük yaşıyorsanız, kullanmayınız! Özyinelemeli metotları yazarken dikkatli olunuz. Özyineleme en çok kombinasyon problemlerini çözmek için elverişli güçlü bir programlama tekniğidir; fakat yanlışlara neden olunabilir ve dikkatsiz kullanma nedeniyle herkese önerilmez. Programın sonlanamaması ve yığın taşmasına neden olması durumlarında, yinelemeli yaklaşımlarla probleme bir çözüm geliştirmeye, problemin tanımı analiz edildikten sonra karar verilmelidir. Örneğin, labirentte en kısa çıkış yolunu bulmak ile tanımlı problemi analiz ederseniz özyineleme kullanmadan, sadece (BFS) Breadth-First Search yardımıyla çözebilirsiniz. Kuyruk (queue) veri yapısı ile uygulamaları bulunan “BFS” algoritması Wavefront algoritması olarak da bilinir. http://en.wikipedia.org/wiki/Breadth-first_search makalesindeki “BFS” algoritması hakkında Wikipedia’da daha fazla bilgi bulabilirsiniz. www.nakov.com C# İle Bilgisayar Programlama Temelleri 361 Alıştırmalar 1. n iç içe döngüyü simüle eden bir program geliştirin. 2. k sınıfından n eleman için tüm çeşitlemeleri tekrarlarıyla listeleyen bir özyinelemeli program geliştirin. Örnek girdi: Çıktısı: Aynı görev için yinelemeli algoritma uygulayın. 3. n elemanlı bir küme içinden k elemanlı tüm kombinasyonları tekrarlarıyla listeleyen bir özyinelemeli program geliştirin. Örnek girdi: Çıktısı: Aynı görev için yinelemeli algoritma uygulayın. 4. Verilen bir sözcük dizisi ve k değeri için, sözcük dizisinin k elemanlı tüm öz alt kümelerini listeleyen özyinelemeli program geliştirin. Örnek girdi: Çıktısı: Aynı görev için yinelemeli bir algoritma uygulayın. www.nakov.com C# İle Bilgisayar Programlama Temelleri 362 5. Verilen bir sözcük dizisi için, sözcük dizisinin tüm alt kümelerini listeleyen özyinelemeli program geliştirin. Örnek girdi: Çıktısı: Aynı görev için yinelemeli algoritma uygulayın. 6. Birleştirme-sıralama (merge-sort) algoritmasını özyinelemeli uygulayın. Bu algoritmaya girdi olarak verilen bir dizi önce iki eşit parçaya bölünür ve özyinelemeli olarak sıralanır. Sonrasında sıralanan parçalar bütün diziyi oluşturmak üzere birleştirilir. 7. Verilen bir n tamsayısı için 1, 2, …, n sayılarının tüm permütasyonlarını listeleyen özyinelemeli program geliştirin. Örnek girdi: Çıktısı: Aynı görev için yinelemeli algoritma uygulayın. 8. Verilen bir tamsayı dizisi ve N sayısı için, dizideki sayıların toplamı N olan tüm alt kümelerini bulan özyinelemeli program geliştirin. Örneğin {2, 1 , 3 , -1} dizisi ve N=4 için, toplamı N=4 olan sayılar şöyle elde edilir: 4=2+3-1; 4=3+1. 9. Verilen bir pozitif tamsayı dizisi için, elemanları toplamı S olan herhangi bir alt kümesinin olup olmadığını hesaplayan bir program geliştirin. Büyük diziler için program verimli çalışabilir mi? 10.Verilen iki pozisyon için, geçerli ve geçersiz kapıları olan bir labirent üzerinde iki pozisyon arasında bulunan tüm yolları hesaplayan bir program geliştirin. 11.Labirentin en kısa yolunu bulmak için BFS (breadth-first search) algoritmasını uygulayan bir program geliştirin. www.nakov.com C# İle Bilgisayar Programlama Temelleri 363 12.İki pozisyon arasındaki tüm yolları hesaplayan bir önceki alıştırmada istenen programı iki pozisyon arasında bir yol olup olmadığını kontrol edecek şekilde değiştirin. 13.Geçerli ve geçersiz kapıları olan verilen bir labirent için tek doğrultuda yol dönüş yapmaksızın, başka tarafa sapmaksızın, komşu ve ardışık pozisyonlardan oluşan en büyük alanı hesaplayan program geliştirin. 14.C:\ hard diski içindeki tüm klasör ve dosyaları ziyaret eden ve özyinelemeli listeleyen bir program geliştirin. www.nakov.com