Açık Veri Yapıları – Java ile Baskı: 0.1F Yazar: Pat Morin© Telif Hakları “Açık Veri Yapıları – Java ile” Pat Morin tarafından yazılmıştır. Her hakkı saklıdır©. Kitabın orijnali https://archive.org/details/ost-computer-science-ods-java adresinde bulunmaktadır. Bu kitap http://creativecommons.org/licenses/by/4.0 ile belirtilen hüküm ve koşullara tabi olarak Türkçe’ye çevrilmiştir. Bu eseri her boyut ve formatta paylaşabilir — kopyalayabilir ve çoğaltabilirsiniz. • Materyalden Adapte et — karıştırabilir, aktarabilir ve eserin üzerine inşa edebilirsiniz. her türlü amaç için, ticari amaç da dahil Lisansör lisans şartlarını takip ettiğiniz sürece özgürlükleri iptal edemez. Bu kitabın Türkçe çevirisi de aynı lisanslamaya tabidir. İÇİNDEKİLER TABLOSU BÖLÜM 1 7 Giriş 7 1.1 Verimlilik Gereksinimi 8 1.2 Arayüzler 1.2.1 Kuyruk, Yığıt ve İki Başlı Kuyruk Arayüzleri 1.2.2 Liste Arayüzü : Doğrusal Diziler 1.2.3 UKüme Arayüzü : Sıralı Olmayan Kümeler 1.2.4 SKüme Arayüzü : Sıralı Kümeler 10 11 13 14 15 1.3 Matematiksel Zemin 1.3.1 Üslüler ve Logaritmalar 1.3.2 Faktöryeller 1.3.3 Asimptotik Gösterim 1.3.4 Rasgele Sıralama ve Olasılık 16 16 18 19 24 1.4 Hesaplama Modeli 26 1.5 Doğruluk, Zaman Karmaşıklığı ve Alan Karmaşıklığı 27 1.6 Kod Örnekleri 29 1.7 Veri Yapıları Listesi 30 1.8 Tartışma ve Alıştırmalar 31 BÖLÜM 2 35 Dizi-Tabanlı Listeler 35 2.1 Dizi Yığıtı: Dizi Kullanan Hızlı Yığıt İşlemleri 2.1.1 Temel Bilgiler 2.1.2 Büyültme ve Küçültme 2.1.3 Özet 36 36 38 41 2.2 Hızlı Dizi Yığıtı: Optimize Dizi Yığıtı 41 2.3 Dizi Kuyruğu : Dizi-Tabanlı Kuyruk 2.3.1 Özet 42 46 2.4 Dizi İki Başlı Kuyruk: Bir Dizi Kullanan Hızlı İki Başlı Kuyruk İşlemleri 2.4.1 Özet 47 49 2.5 Çifte Dizi İki Başlı Kuyruk: İki Yığıt’tan Bir İki Başlı Kuyruk Oluşturulması 50 2.5.2 Özet 56 2 2.6 Kök Dizi Yığıt: Alan Kriteri Verimli Olan Bir Dizi Yığıt Uygulaması 2.6.1 Büyültme ve Küçültmenin Analizi 2.6.2 Alan Kullanımı 2.6.3 Özet 2.6.4 Karekökleri Hesaplamak 56 61 62 63 63 2.7 67 Tartışma ve Alıştırmalar BÖLÜM 3 70 Bağlantılı Listeler 70 3.1 TBListe: Tekli-Bağlantılı Liste 3.1.1 Kuyruk İşlemleri 3.1.2 Özet 70 72 73 3.2 ÇBListe: Çifte-Bağlantılı Liste 3.2.1 Ekleme ve Çıkarma 3.2.2 Özet 74 75 77 3.3 AVListe: Alan-Verimli Liste 3.3.1 Alan Gereksinimleri 3.3.2 Elemanların Bulunması 3.3.3 Elemanların Eklenmesi 3.3.4 Elemanların Silinmesi 3.3.5 yayıl (u) ve birarayaGetir (u) Yöntemlerinin Amortize Edilmiş Analizi 3.3.6 Özet 78 79 79 81 84 86 88 3.4 88 Tartışma ve Alıştırmalar BÖLÜM 4 93 Sekme Listeleri 93 4.1 93 Temel Yapı 4.2 Sıralı Küme Sekme Listesi : Verimli bir Sıralı Küme 4.2.1 Özet 95 98 4.3 Liste Sekme Listesi: Verimli Rasgele Erişim Listesi 4.3.1 Özet 99 103 4.4 Sekme Listelerinin Analizi 104 4.5 Tartışma ve Alıştırmalar 108 3 BÖLÜM 5 112 Karma Tabloları 112 5.1 Zincirleme Karma Tablo: Zincirleme Aracılığıyla Adresleme 5.1.1 Çarpımsal Karma Yöntemi 5.1.2 Özet 112 115 119 5.2 Doğrusal Karma Tablo: Doğrusal Yerleştirme 5.2.1 Doğrusal Yerleştirme Analizi 5.2.2 Özet 5.2.3 Listeleme Karma Yöntemi 120 123 127 127 5.3 Karma Kodları 5.3.1 Temel Veri Türleri için Karma Kodları 5.3.2 Bileşik Nesneler için Karma Kodları 5.3.3 Diziler ve Dizeler için Karma Kodları 128 129 129 132 5.4 135 Tartışma ve Alıştırmalar BÖLÜM 6 140 İkili Ağaçlar 140 6.1 İki Dallı Ağaç: Temel İkili Ağaç 6.1.1 Özyinelemeli Algoritmalar 6.1.2 İkili Ağaçta Sıralı-Düğüm Ziyaretleri 142 143 143 6.2 Serbest Yükseklikli İkili Arama Ağacı 6.2.1 Arama 6.2.2 Ekleme 6.2.3 Silme 6.2.4 Özet 146 147 148 150 152 6.3 153 Tartışma ve Alıştırmalar BÖLÜM 7 157 Rasgele İkili Arama Ağaçları 157 7.1 Rasgele İkili Arama Ağaçları 7.1.1 Önerme 7.1’in Kanıtı 7.1.2 Özet 157 160 163 7.2 Treap: Randomize İkili Arama Ağaçları 7.2.1 Özet 163 172 7.3 173 Tartışma ve Alıştırmalar 4 BÖLÜM 8 178 Günah Keçisi Ağaçları 178 8.1 Günah Keçisi Ağacı: Kısmi Yeniden Oluşturmalı İkili Arama Ağacı 8.1.1 Doğruluk Analizi ve Çalışma Zamanı 8.1.2 Özet 178 183 186 8.2 187 Tartışma ve Alıştırmalar BÖLÜM 9 190 Kırmızı-Siyah Ağaçlar 190 9.1 2-4 Ağacı 9.1.1 Yaprak eklenmesi 9.1.2 Yaprak silinmesi 191 191 193 9.2 Kırmızı-Siyah Ağacı: 2-4 Ağacı ile Benzeşen 9.2.1 Kırmızı-Siyah Ağaçlar ve 2-4 Ağaçlar 9.2.2 Kırmızı-Siyah Ağaçlarda Sola-dayanım 9.2.3 Ekleme 9.2.4 Silme 194 195 198 200 203 9.3 Özet 208 9.4 Tartışma ve Alıştırmalar 209 BÖLÜM 10 213 Yığınlar 213 10.1 İkili Yığın: Bir Örtülü İkili Ağaç 10.1.1 Özet 213 218 10.2 Karışık Yığın: Rasgele Karışık Yığın 10.2.1 merge(h1, h2) Analizi 10.2.2 Özet 218 221 223 10.5 223 Tartışma ve Alıştırmalar BÖLÜM 11 226 Sıralama Algoritmaları 226 11.1 Karşılaştırmaya-Dayalı Sıralamalar 11.1.1 Birleştirerek-Sıralama 11.1.2 Hızlı-Sıralama 11.1.3 Yığın-Sıralama 11.1.4 Karşılaştırmaya Dayalı Sıralama için Alt-Sınır 227 227 231 235 237 5 11.2 Sayma Sıralama ve Taban Sıralaması 11.2.1 Sayma Sıralaması 11.2.2 Taban-Sıralaması 240 241 243 11.3 245 Tartışma ve Alıştırmalar BÖLÜM 12 248 Grafikler 248 12.1 Bitişiklik Matrisi: Bir Grafiğin Matris ile Gösterimi 250 12.2 Bitişiklik Listeleri: Liste Derlemi olarak Grafik 253 12.3 Grafik Ziyaretleri 12.3.1 Enine-Arama 12.3.2 İlkin-Derinliğine Arama 257 257 259 12.4 262 Tartışma ve Alıştırmalar BÖLÜM 13 266 Tamsayılar için Veri Yapıları 266 13.1 İkili Sıralı Ağaç: Sayısal Arama Ağacı 267 13.2 X-Hızlı Sıralı Ağaç: Çifte-Logaritmik Zamanda Arama 272 13.3 Y-Hızlı Sıralı Ağaç: Çifte-Logaritmik Zamanlı Sıralı Küme 275 13.4 Tartışma ve Alıştırmalar 281 BÖLÜM 14 283 Dış Bellek Aramaları 283 14.1 284 Blok Deposu 14.2 B-Ağaçları 14.2.1 Arama 14.2.2 Ekleme 14.2.3 Silme 14.2.4 B-Ağaç’ların Amortize Analizi 285 288 290 294 300 14.3 303 Tartışma ve Alıştırmalar KAYNAKÇA 308 6 Bölüm 1 Giriş Dünyadaki bilgisayar bilimleri öğretim programlarının tümünün bir parçası olarak veri yapıları ve algoritmalar üzerine bir ders bulunur. Veri yapıları bu kadar önemlidir, her gün, hafta ve ay aynı zamanda bizi tehlike, zarar ve başarısızlıktan korur; böylece daha başarılı oluruz. Pek çok multi-milyon ve multi-milyar liralık şirketler ihtiyaçları veya gereksinimlerine uygun birçok veri yapısı etrafında inşa edilmişlerdir. Bu nasıl olabilir? Eğer biraz düşünürsek, veri yapıları ile belirli bir süre boyunca çok sık ya da her zaman etkileşim içinde olduğumuzu anlarız. • Dosya açmak: Dosya sistemine ait veri yapıları bir dosyanın parçalarını diskte bulmak için kullanılır, böylece o dosyaya erişebiliriz. Bu kolay değildir; diskler yüzlerce milyon blok içerir. Dosyanızın içeriği bunlardan herhangi biri üzerinde saklanmış olabilir. • Telefonunuzda kayıtlı olan bir kişiye bakmak: Siz aramayı/yazmayı bitirmezden önce kısmi bilgiye dayalı olarak kişi listesindeki bir telefon numarasını aramak için de bir veri yapısı kullanılır. Bu kolay değildir; telefonuzda pek çok insan hakkında bilgiler kayıtlı olabilir - telefon ya da e-posta yoluyla temas etmiş olduğunuz herkes - ve telefonunuzun çok hızlı bir işlemcisi ya da belleği yoktur. • En sevdiğiniz sosyal ağınıza giriş yapmak: Ağ sunucuları hesap bilgilerinize bakmak için giriş bilgilerinizi kullanır. Bu kolay değildir; en popüler sosyal ağlar yüzlerce milyon aktif kullanıcı içerir. • Web araması yapmak: Arama motoru, arama terimlerinizi içeren web sayfalarını bulmak için veri yapılarını kullanır. Bu kolay değildir; Internet üzerinde 8,5 milyarın üzerinde web sayfası vardır ve her sayfa pek çok potansiyel arama terimlerini içerir. 7 • Telefon acil durum hizmetleri (1-5-5): Polis arabaları, ambulanslar ve itfaiye araçları gecikmeden adresinize gönderilsin diye acil servis şebekeleri telefon numaralarını adreslere eşleştiren bir veri yapısı içinde sizin telefon numaranızı arar. Bu önemlidir, çağrıyı yapan kişinin tam adresini vermesi mümkün olmayabilir ve bir gecikme ölüm kalım arasındaki fark anlamına gelebilir. 1.1 Verimlilik Gereksinimi Bir sonraki bölümde, en sık kullanılan veri yapıları tarafından desteklenen işlemlere bakacağız. Biraz programlama deneyimi olan herkes bu işlemleri doğru olarak gerçekleştirmenin zor olmadığını görecektir. Muhtemelen bir dizi veya bir bağlantılı listede veriyi saklayabiliriz ve dizi veya listedeki tüm elemanlar üzerinden yineleme yaparak örneğin bir öğeyi ekleyebilir veya kaldırabiliriz. Bu tür bir uygulama kolaydır, ancak çok verimli değildir. Bu gerçekten önemli mi? Bilgisayarlar hızlı ve gitgide daha hızlı hale gelmektedir. Belki anlaşılması kolay olan uygulama yeterince iyidir. Öğrenmek için biraz taslak hesaplama yapalım. İşlem sayısı: 1 milyon (106) elemanlı olan orta ölçekte veri seti kullanan bir uygulama düşünün. Bu uygulamanın en az bir kez her bir elemanı aramak isteyeceğini varsaymak, çoğu uygulama için makuldur. Bu demektir ki, bu veriler içinden en az 1 milyon (106) arama yapmayı bekleyebiliriz. Her bir arama için 106 inceleme yapılacaksa, bu toplam 106 x 106 = 1012 (bir trilyon) arama verir. İşlemci hızları: Çok hızlı bir masaüstü bilgisayar bile saniyede bir milyardan fazla (109) yazma işlemi yapamaz.1 Bu demektir ki, bu uygulama en az 1012 / 109 = 1000 saniye, ya da yaklaşık 16 dakika ve 40 saniye alacaktır. Onaltı dakika bilgisayar zamanı için bir ebediyettir, ancak kahve molası vermek için dışarıya yönelen bir kişi ona katlanabilir. Büyük veri setleri: Şimdi 8,5 milyarın üzerinde web sayfasını endeksleyen, Google gibi bir şirket düşünün. Bizim hesaplamalara göre, bu veriler üzerinde yapılacak her türlü sorgu en az 1 Bilgisayar hızları çoğu birkaç gigahertz (saniyede milyarlarca devir) civarındadır ve her işlem genellikle birkaç devir alır. 8 8,5 saniye sürer. Ancak bu durumda olmadığımızı zaten biliyoruz; web aramaları 8,5 saniyeden çok daha az sürede tamamlanıyor, ve belirli bir sayfanın kendilerine endeksli sayfaların içinde olup olmadığını sormaktan çok daha karmaşık sorguları yapıyorlar. Google, saniyede yaklaşık 4.500 sorgu alır, bu onların işi yürütebilmesi için en az 4.500 x 8,5 = 38.250 çok hızlı sunucuya gereksinim duydukları anlamına gelir. Çözüm: Bu örnekler veri yapılarının anlaşılması kolay olan uygulamalarındaki, n, veri yapısındaki eleman sayısının ve, m, veri yapısı üzerinde gerçekleştirilen işlem sayısının, özellikle her ikisi de büyük olduğunda iyi ölçek sağlamadığını bize söylemektedir. Bu durumda, zaman (diyelim ki, makine komutu cinsinden ölçüldüğünde) yaklaşık olarak n x m olacaktır. Çözüm, tabii ki, her işlemin her veri elemanını kontrol etmesini gerektirmeyecek şekilde veri yapısı içindeki verileri dikkatle organize etmektir. Bu ilk bakışta imkansız gözükmesine rağmen, veri yapısı içinde depolanmış eleman sayısından bağımsız olacak şekilde, sadece ortalama iki elemana bakarak arama yapılabilen veri yapılarını göreceğiz. Saniyede milyar komut çalıştırabilen bilgisayarımızda, bir milyar eleman (veya bir trilyon, bir katrilyon, ve hatta bir kentilyon) içeren bir veri yapısı içinde arama yapmak sadece 0,000000002 saniye alır. Elemanları sıralı bir şekilde tutan ve bir işlem sırasında kontrol edilen eleman sayısının, veri yapısındaki eleman sayısının bir fonksiyonu olarak, çok yavaş büyüdüğü gerçekleştirimleri de göreceğiz. Örneğin, herhangi bir işlem sırasında en fazla 60 elemanı kontrol edecek şekilde bir milyar elemanı sıralı olarak tutabiliriz. Saniyede milyar komut çalıştırabilen bilgisayarımızda, bu işlemler 0,00000006 saniye alır. Bu bölümün kalan kısmında kısaca kitabın geri kalanı boyunca kullanılan bazı temel kavramlar yorumlanacaktır. Bölüm 1.2, bu kitapta anlatılan veri yapılarının tümü tarafından gerçekleştirilen arayüzleri açıklamaktadır ve okunması gerekli olarak dikkate alınmalıdır. Kalan bölümler şunları tartışmaktadır: • üsler, logaritma, faktöriyel, asimptotik (büyük-Oh) gösterimi, olasılık ve randomizasyon dahil olmak üzere bazı matematiksel incelemeler; • hesaplama modeli; 9 • doğruluk, yürütme zamanı ve alanı; • kalan bölümlere genel bir bakış, ve • örnek kod ve dizgi kuralları. Bu alanlarda birikim sahibi olan veya olmayan okuyucular şimdi onları kolayca atlayabilir ve gerekirse daha sonra onlara tekrar geri dönebilir. 1.2 Arayüzler Veri yapılarını tartışırken, bir veri yapının arayüzü ile gerçekleştirimi arasındaki farkı anlamak önemlidir. Bir arayüz bir veri yapısının ne yaptığını açıklarken, gerçekleştirimi veri yapısının bunu nasıl yaptığını açıklar. Bazen soyut veri türü olarak da adlandırılan arayüz, bir veri yapısı tarafından desteklenen işlemlerin kümesini ve bu işlemlerin semantiğini, yani anlamını, tanımlar. Bir arayüz bize veri yapısının bu işlemleri nasıl gerçekleştirdiğine dair hiçbir şey söylemez; sadece desteklenen işlemlerin bir listesini ve her işlemin ne tür argümanları kabul ettiğini ve döndürdükleri değer hakkında belirtimler sağlar. Bir veri yapısı gerçekleştirimi, diğer taraftan, veri yapısının iç gösterimini ve aynı zamanda veri yapısı tarafından desteklenen işlemleri uygulamaya koyan algoritmaların tanımlarını içerir. Bu nedenle, tek bir arayüzün birçok uygulaması olabilir. Örneğin biz, Bölüm 2’de, dizileri kullanarak Liste arayüzü uygulamalarını görürken, Bölüm 3’te işaretçi-tabanlı veri yapılarını kullanarak Liste arayüzü uygulamalarını göreceğiz. Her ikisi de aynı arayüzü, Liste’yi uygular, ama farklı şekillerde. 10 1.2.1 Kuyruk, Yığıt ve İki Başlı Kuyruk Arayüzleri Kuyruk arayüzü kendisine eleman ekleyebileceğimiz ve sonraki elemanı kaldırabileceğimiz bir elemanlar topluluğunu simgeler. Daha kesin olarak, Kuyruk arayüzü tarafından desteklenen işlemler şunlardır: • ekle (x): Kuyruğa x değerini ekler. • sil (x): Sonraki (daha önceden eklenen) y değerini, kuyruktan kaldırır ve y değerini döndürür. sil () işleminin hiçbir argüman almadığına dikkat edin. Kuyruk ’un kuyruklama disiplini hangi elemanın kaldırılması gerektiğine karar verir. En yaygın olarak FIFO (ilk-giren-ilkçıkar), öncelik kuyruğu, ve LIFO (son-giren-ilk-çıkar) dahil olmak üzere birçok olası kuyruk disiplinleri vardır. Şekil 1.1 'de gösterilen bir FIFO (ilk-giren-ilk-çıkar) Kuyruk ’u , elemanları onların eklendiği sıranın aynı sırasıyla kaldırır. Bunu bir bakkalın kasasında çalışan kuyruğa (veya sıraya) benzetebiliriz. Bu en yaygın Kuyruk türüdür, bu nedenle FIFO sıfatı genellikle ihmal edilir, kullanılmaz. Diğer metinlerde genellikle, FIFO Kuyruğu üzerindeki ekle (x) ve sil() işlemleri sırasıyla kuyruğa koy (x) ve kuyruktan kaldır () olarak adlandırılır. Şekil 1.2 'de gösterilen öncelik kuyruğu, her zaman, en küçük elemanı siler, bağları buna uygun olacak şekilde kırar. Bunu bir hastanenin acil serviste hastalara servis vermesine benzetebiliriz. Hastalar geldikçe değerlendirilir ve daha sonra bir bekleme odasına yerleştirilir. Bir doktor müsait olduğunda o ilk olarak yaşam tehditi en fazla bulunan hasta ile ilgilenir. Öncelik kuyruğu üzerindeki sil () işlemi diğer metinlerde genellikle silMin () olarak adlandırılır. Çok yaygın bir kuyruk disiplini de Şekil 1.3 'de gösterildiği gibi LIFO (son-giren-ilk-çıkar) disiplinidir. LIFO Kuyruğunda, en son eklenen eleman silinir. Bunu en iyi tabakların üst üste dizildiği bir tabak yığıtı şeklinde görselleştirebiliriz. Yeni bir tabağı yığıtın en üstüne dizerken, yine tabak çıkarırken yığıtın en üstünden çıkarırız. Bu yapı çok yaygındır ve Yığıt adı altında incelenir. Bir Yığıt tartışılırken çoğu zaman, ekle () ve sil () adları yığ() ve 11 yığıttan kaldır () olarak değiştirilir, bunun nedeni LIFO ve FIFO kuyruk disiplinleri ile karışmasının önüne geçmektir. İki Başlı Kuyruk, hem FIFO Kuyruğu ve hem de LIFO Kuyruğu’nun (Yığıt) genellenmiş bir halidir. Önü ve arkası bulunan elemanlardan oluşan bir diziyi gösterir. Elemanlar dizinin başına veya sonuna eklenebilir. İki Başlı Kuyruk işlemlerinin adları da kendi kendilerini açıklayıcıdır: başaEkle(x), baştanSil (), sonaEkle (x), sondanSil (). Şunu belirtmeliyiz ki, bir Yığıt sadece başaEkle (x) ve baştanSil () kullanılarak uygulanabilirken, bir FIFO kuyruk da sonaEkle (x) ve sondanSil () kullanılarak uygulanabilir. 12 1.2.2 Liste Arayüzü : Doğrusal Diziler Bu kitapta FIFO Kuyruk, Yığıt, ya da İki Başlı Kuyruk arayüzleri hakkında çok az konuşacağız. Bunun nedeni bu arayüzlerin Liste arayüzü tarafından kapsanmasıdır. Şekil1.4’te gösterilmiş olan bir Liste, x0 … xn-1, değerlerinden oluşan bir diziyi simgeler. Liste arayüzü aşağıdaki işlemleri içerir: 1. boyut () : Listenin uzunluğunu, n, döndür. 2. al (i) : xi değerini döndür. 3. belirle (i, x) : xi değerini x ’e eşitle. 4. ekle (i, x) : x1 … xn-1 ’in yerini değiştirerek i konumuna x değerini yerleştir; Her j ∈ { n-1, …, i } için xj+1 = xj eşitle, n ’i 1 artır, ve xi = x eşitle. 5. sil (i) : xi+1 … xn-1 ’in yerini değiştirerek xi değerini kaldır; Her j ∈ { i, …, n-2 } için xj = xj+1 eşitler, n ’i 1 azalt. Dikkat ediniz ki, bu işlemler İki Başlı Kuyruk arayüzünü uygulamak için de kesinlikle yeterlidir. başaEkle (x) ⇒ ekle (0, x) baştanSil () ⇒ sil (0) sonaEkle (x) ⇒ ekle (boyut (), x) sondanSil () ⇒ sil (boyut () – 1) Daha sonraki bölümlerde Yığıt, İki Başlı Kuyruk ve FIFO Kuyruk arayüzlerini tartışmayacağımıza rağmen, Yığıt ve İki Başlı Kuyruk terimleri bazen Liste arayüzünü 13 gerçekleştiren veri yapılarını adlandırmakta kullanılmaktadır. Bu durum, bu veri yapılarının Yığıt veya İki Başlı Kuyruk arayüzlerini çok verimli uygulamak için kullanılabilir olması gerçeğini vurgulamaktadır. Örneğin, DiziİkiBaşlıKuyruk sınıfı tüm İki Başlı Kuyruk işlemlerini sabit zamanda gerçekleştiren bir Liste arayüzünün uygulamasıdır. 1.2.3 UKüme Arayüzü : Sıralı Olmayan Kümeler UKüme arayüzü matematiksel bir diziyi taklit eden tekil elemanlardan oluşan ve sıralı olmayan bir kümeyi simgeler. Bir UKüme n farklı eleman içerir; hiçbir eleman bir kereden fazla görünmez; ve elemanlar hiçbir belirli sırada sıralanmamıştır. Bir UKüme aşağıdaki işlemleri destekler: 1. boyut () : Kümedeki eleman sayısı olan, n, döndür. 2. ekle (x) : Kümede zaten mevcut değilse x elemanını kümeye ekle; Kümede hiçbir y elemanı yoktur ki x y ’ye eşit olsun koşuluyla x elemanını kümeye ekle. x kümeye eklendiği takdirde doğru döndür ve aksi takdirde yanlış döndür. 3. sil (x) : Kümeden x ’i kaldır; Kümede öyle bir y elemanı bul ki x y ’ye eşit olsun ve bu durumda y ’yi sil. Bu tür bir eleman yoksa null, varsa y döndür. 4. bul (x) : Kümede varsa, x elemanını bul; Kümede öyle bir y elemanı bulur ki y x ’e eşit olsun. Bu tür bir eleman yoksa null, varsa y döndür. Bu tanımlar sildiğimiz veya bulduğumuz değer olan x ile silebileceğimiz veya kaldırabileceğimiz değer olan y arasındaki ayrımı biraz belirsiz yapıyor. Bunun nedeni x ve y eşit olarak değerlendirilirse de aslında farklı nesneler olabilir.2 Böyle bir ayrım yararlıdır, çünkü anahtarları değerlerle eşleştiren sözlük veya eşlemlerin oluşturulmasına izin verir. Bir sözlük / eşlem oluşturmak için, her biri bir anahtar ve bir değer içeren Çiftler diye adlandırılan bileşik nesneleri oluşturmak gerekir. İki Çift, anahtarları eşitse eşit olarak kabul edilir. (k, v) çiftini bir UKüme’de saklarsak ve daha sonra x = (k, null) çiftini kullanarak 2 Bu Java'da, sınıfın eşittir (y) ve hashCode () yöntemleri geçersiz kılınarak yapılır. 14 bul (x) yöntemini çağırırsak, sonuç y = (k, v) olacaktır. Diğer bir deyişle, sadece anahtar, k değeri verilerek, v ’yi geri kazanmak mümkündür. 1.2.4 SKüme Arayüzü : Sıralı Kümeler SKüme arayüzü sıralanmış elemanların bir kümesini simgeler. SKüme elemanlarının tamamı sıralı halde saklanır, böylece herhangi iki eleman, x ve y karşılaştırılabilir. Kod örneklerinde bu, tanımı aşağıdaki gibi olan ve karşılaştır (x, y) adı verilen bir yöntemle yapılacaktır. SKüme, boyut (x), ekle(x), sil (x) yöntemlerini UKüme arayüzüyle tam olarak aynı anlamda olacak şekilde destekler. Bir UKüme ve SKüme arasındaki fark bul (x) yöntemindedir : 4. bul (x) : Sıralı kümede x ’i bulur. Kümede öyle bir en küçük y elemanı bul ki y ≥ x. Bu tür bir eleman yoksa null, varsa y döndür. Bu tarzdaki bul (x) işlemi bazen bir ardıl arama olarak adlandırılır. UKüme.bul(x) ’ten temel bir şekilde farklıdır, çünkü kümede x eşiti herhangi bir eleman olmasa bile anlamlı bir sonuç döndürür. UKüme ve SKüme bul (x) işlemleri arasındaki ayrım çok önemlidır ve çoğu kez cevapsızdır. Bir SKüme tarafından sağlanan ekstra işlevsellik genellikle hem daha büyük bir çalışma süresi ve hem de daha yüksek bir uygulama karmaşıklığını içeren bir eder ile birlikte gelir. Örneğin, bu kitapta tartışılan SKüme uygulamalarının çoğunda bul (x) işlemleri küme boyutu ile logaritmik bir çalışma süresine sahiptir. Öte yandan, Bölüm 5 ’teki gibi UKüme ’nin ZincirliKarımTablosu olarak uygulamasında sabit beklenen zamanda çalışan bir bul(x) işlemi vardır. Hangi yapıyı seçeceğinize karar verirken, SKüme tarafından sunulan ekstra işlevsellik gerçekten gerekli olmadıkça, her zaman bir UKüme kullanmalısınız. 15 1.3 Matematiksel Zemin Bu bölümde, biz, logaritma, büyük-Oh gösterimi ve olasılık teorisi de dahil olmak üzere bu kitap boyunca kullanılan bazı matematiksel gösterimler ve araçları gözden geçireceğiz. Bu yorum kısa olacaktır ve bir giriş olarak tasarlanmamıştır. Birikimlerinin eksik olduğunu hisseden okuyucuları, bilgisayar bilimleri için matematik [50] konusunda yazılmış çok iyi (ve ücretsiz) ders kitabının uygun bölümlerini okumaları ve alıştırmalar yapmaları yönünde teşvik ediyoruz. 1.3.1 Üslüler ve Logaritmalar bx ifadesi b sayısının x üssüne yükseltilmiş halini gösterir. x pozitif bir tamsayıysa, bu sadece b değerinin x – 1 defa kendisiyle çarpılmış haline eşittir: x negatif bir tam sayı olduğu zaman, bx = 1 / b-x . x = 0 iken, bx = 1. b tamsayı değilse, biz hala üssel fonksiyonu, üssel seri cinsinden tanımlanan ex (aşağıya bkz.) açısından tanımlayabiliriz, ama en iyisi bunu bir hesap analizi kitabına bırakalım. Bu kitapta, log b k ifadesi k’nın taban-b logaritmasını ifade eder. Yani, tekil bir x değeri aşağıdaki koşulu sağlar. Bu kitaptaki logaritmaların çoğu taban 2’dir (ikili logaritmadır). Bunlar için, tabanı ihmal ederiz, böylece log 2 k için kısaltma olarak log k kullanılır. 16 Logaritmaları düşünmenin biçimsel, ama yararlı bir yolu da log b k için sonuç 1'e eşit ya da daha az olana dek k’nın b’ye kaç defa bölündüğünü bulmaktır. Örneğin, bir ikili arama yaptığımızda, her bir karşılaştırma, muhtemel cevapların sayısını 2 faktörü kadar azaltır. Olası en çok bir cevap kalana kadar bu tekrarlanır. Bu nedenle, başlangıçta en fazla n + 1 olası cevap olduğunda ikili arama tarafından yapılan karşılaştırma sayısı en fazla log 2 (n + 1) ’dir. Bu kitapta birkaç kez ortaya çıkan başka bir logaritma türü de doğal logaritmadır. Burada log e k belirtimi için ln k gösterimini kullanırız. e – Euler sabiti – olarak aşağıda verilmiştir: Doğal logaritma sık sık gündeme gelir, çünkü bu aşağıda verilen özellikle yaygın bir integralin değeridir: Logaritmalar ile en çok yaptığımız iki çalışmadan birincisi onları bir üsten kaldırmaktır: ve ikincisi logaritmanın tabanını değiştirmektir: Örneğin, doğal ve ikili logaritmayı karşılaştırmak için bu iki işlemi kullanabilirsiniz: 17 1.3.2 Faktöryeller Bu kitapta, bir ya da iki yerde, faktöryel fonksiyonu kullanılmıştır. Negatif olmayan bir n tamsayısı için, n! (okunuşu "n faktöryel") gösterimi şu anlamda tanımlanmıştır : Faktöryeller ortaya çıkmıştır çünkü n!, n farklı elemanın permütasyon sayısını; yani sıralamasını, sayar. Özel durum n = 0 için, 0! = 1 olarak tanımlanmaktadır. n! sayısı Stirling'in Yaklaşımı kullanılarak yaklaşık olarak tahmin edilebilir : Burada Stirling'in Yaklaşımı ln (n!) değerini de yaklaşık olarak tahmin eder. (Aslında, Stirling'in Yaklaşımı’nın en kolay kanıtı ln (n!) = ln 1 + ln 2 + … + ln n ifadesinin değerini yaklaşık olarak integrali ile tahmin etmektir.) Faktöryel fonksiyonu ile ilgili olarak iki terimli katsayılara değineceğiz. Negatif olmayan bir tamsayı n ve bir tamsayı k ∈ {0, …, n} için şu gösterim doğrudur: 18 İki terimli katsayı (okunuşu : “n’in k tercihi”) n elemanlı bir kümenin k elemanlı alt kümelerinin sayısını sayar; yani {1, …, n} kümesinden k farklı tamsayıyı seçme yollarının sayısıdır. 1.3.3 Asimptotik Gösterim Bu kitapta veri yapılarını analiz ederken, çeşitli işlemlerin çalışma zamanları hakkında konuşmak istiyoruz. Tam kesinlikte çalışma zamanları, tabii ki, bilgisayardan bilgisayara ve hatta tek bir bilgisayar üzerinde çalıştırmadan çalıştırmaya değişecektir. Bir işlemin çalışma süresi hakkında konuştuğumuzda, işlem sırasında gerçekleştirilen bilgisayar komutları sayısına başvuruyoruz. Basit bir kod için bile, bu miktarı tam olarak hesaplamak zor olabilir. Bu nedenle, çalışma zamanlarını tam kesinlikte analiz etmek yerine biz, büyük-Oh adı verilen notasyonu kullanacağız: Bir f (n) fonksiyonu için, O (f (n)) bir dizi fonksiyon gösterir; öyle ki; Grafiksel düşünüldüğünde, bu küme fonksiyonları g (n) fonksiyonlarından oluşur; burada c f(n), n yeterince büyük olduğunda, g (n) ’e başat olmaya başlar. Biz genellikle fonksiyonları kolaylaştırmak için asimptotik gösterimi kullanırız. Örneğin 5n log n + 8n – 200 yerine O (n lg n) yazabiliriz. Bu şu şekilde kanıtlanmıştır : 19 Bu gösteriyor ki, 5n log n + 8n – 200 fonksiyonu c = 13 ve n0 = 2 için O (n lg n) kümesindedir. Asimptotik notasyonu kullanırken bir dizi faydalı kısayollar uygulanabilir. Birincisi : herhangi bir c1 < c2 için. İkincisi: Herhangi a, b, c > 0 , sabit olmak şartıyla, Bu kapsama bağıntıları herhangi bir pozitif değer ile çarpıldığında hala geçerli olur. Örneğin, n çarpıldığında şunu elde ederiz : Uzun ve seçkin bir geleneğin devamı olarak, biz bu notasyonu kötüye kullanarak gerçekten f1(n) ∈ O( f(n) ) demek istediğimizde f1(n) = O( f(n) ) yazacağız. Yine, “bu işlemin yürütme süresi O( f (n) ) ’dir.” gibi cümleler kuracağız. Gerçekte bu cümle “bu işlemin yürütme süresi O( f (n) ) ’in bir üyesidir.” olmalıdır. Bu kısayollar çoğunlukla asimptotik notasyonu daha kolay denklem dizeleri içinde kullanmamızı sağlar. Aşağıdaki gibi ifadeler yazarken, özellikle garip bir örnek ortaya çıkar : Yine, bu daha doğru bir şekilde şöyle yazılmalıdır: O (1) ifadesi başka bir konuyu da gündeme getiriyor. Bu ifadede hiçbir değişken bulunmadığından, hangi değişkenin rasgele büyük olduğu açık olmayabilir. Bağlam olmadan 20 bunu söylemenin hiçbir yolu yoktur. Yukarıdaki örnekte, denklemin geri kalanındaki tek değişken n olduğu için, denklemi şöyle okumamız gerektiğini varsayabiliriz : f(n) = 1 iken; T(n) = 2 log(n) + O (f(n)) . Büyük-Oh notasyonu bilgisayar bilimleri için yeni ve benzersiz değildir. Sayı kuramcısı Paul Bachmann tarafından 1894 gibi erken bir tarihte kullanılmıştır, ve bilgisayar algoritmalarının yürütme zamanlarını açıklamak için son derece yararlıdır. Aşağıdaki kod parçasını düşünün: Bu yöntemin bir uygulaması şu işlemleri içerir : • 1 atama (int i = 0), • n + 1 karşılaştırma (i < n), • n artırım (i ++), • n bağıl konum dizi hesaplaması (a [i]), ve • n dolaylı atama (a [i] = i). Bu yüzden bu yürütme zamanını şöyle yazabiliriz : Burada a, b, c, d ve e, kodu çalıştıran makineye bağlı olan sabitlerdir ve sırasıyla atamaları, karşılaştırmaları, artırım işlemlerini, bağıl konum dizi hesaplamalarını ve dolaylı atamaları gerçekleştirmek için gerekli zamanı simgelerler. Bu ifade iki satır kod çalışma süresini gösterirse de, açıkça bu tür bir analiz karmaşık kod veya algoritmalar için kolay işlenemeyecektir. Büyük-Oh notasyonunu kullanarak, çalışma süresi basitleştirilmiş halde şöyledir: 21 Bu sadece daha küçük boyutlu olmakla kalmaz, ama aynı zamanda yaklaşık olarak aynı bilgiyi vermektedir. Yukarıdaki örnekte yürütme süresinin a, b, c, d ve e sabitlerine bağlı olması, genel olarak, bu sabitlerin değerlerini bilmeden iki çalışma sürelerinden hangisinin daha hızlı olduğunu bilmek için karşılaştırmanın mümkün olamayacağı anlamına gelir. Bu sabitleri belirlemek için (diyelim ki, zamanlama testleri aracılığıyla) çaba göstersek bile, sonuç sadece bizim üzerinde test yaptığımız makine için geçerli olacaktır. Büyük-Oh notasyonu bizim daha karmaşık fonksiyonları çözümlememizi sağlayarak, çok daha yüksek düzeyde akıl ve mantık yürütmemize ve düşünmemize olanak verir. İki algoritmanın aynı büyük-Oh çalışma süresi varsa, hangisinin daha hızlı olduğunu bilemezsiniz, ve net bir kazanan olmayabilir. Biri tek makinede daha hızlı olabilir, ve diğeri farklı bir makinede daha hızlı olabilir. Ancak, iki algoritmanın kanıtlanabilir farklı büyük-Oh çalışma süreleri varsa, o zaman emin olabiliriz ki daha küçük çalışma süresi olan yeterince büyük n değerleri için daha hızlı olacaktır. Büyük-Oh notasyonu bize, iki farklı fonksiyonu karşılaştırmak için olanak tanır. Buna örnek, f1(n) = 15n ve f2(n) = 2n logn fonksiyonlarının büyüme hızlarını karşılaştıran Şekil 1.5 'te gösterilmiştir. Burada f1 (n) karmaşık bir oğrusal zaman algoritmasının yürütme zamanı olabilecekken, f2 (n) böl-ve-yönet paradigmasına dayanan oldukça daha basit bir algoritmanın çalışma zamanı olabilir. Bu göstermektedir ki, f1 (n) küçük n değerleri için f2 (n)'den daha büyük olmasına rağmen, büyük n değerleri için karşıtı geçerlidir. Sonunda f1 (n) giderek artan farkla kazanır. Büyük-Oh gösterimi kullanılarak yapılan analiz bunun böyle olacağını bize anlatmıştı, çünkü O (n) ⊂ O (n log n) . Birkaç durumda, birden fazla değişkenli fonksiyonlar üzerinde de asimptotik gösterimi kullanacağız. Bunun için herhangi bir standart yoktur, ama bizim amaçlarımız için, aşağıdaki tanım yeterlidir: 22 Bu tanım, bizim gerçekten önem verdiğimiz durumu yakalar: n1, …, nk argümanlarının g ’nin büyük değer almasını sağlaması. Bu tanım, aynı zamanda tek değişkenli O(f (n) tanımı ile, f (n) n’in artan bir fonksiyonu olduğu zaman uzlaşır. Bizim amaçlarımız için bu kadarının yeterli olmasına rağmen okuyucu, diğer metinlerde çok değişkenli fonksiyonlar ve farklı asimptotik gösterimlerin de ele alınabileceği konusunda uyarılmalıdır. 23 1.3.4 Rasgele Sıralama ve Olasılık Bu kitapta sunulan veri yapılarının bazıları rasgeleleştirilmiştir, depolanan verilerden veya onların üzerinde yapılan işlemlerden bağımsız rasgele seçimler yaparlar. Bu nedenle, bu gibi yapılar kullanılarak aynı işlem setinin bir kereden fazla çalıştırılması farklı yürütme zamanlarına neden olabilir. Bu veri yapılarını analiz ederken, onların ortalama veya beklenen çalışma süreleri ile ilgileniriz. Kurallı olarak, bir rasgele veri yapısı üzerindeki bir işlemin çalışma süresi rasgele bir değişkendir, ve biz onun beklenen değerini incelemek istiyoruz. Değerlerini bazı sayılabilir U evreninden alan bir süreksiz rasgele değişken X için, E [X] ile gösterilen, X ’in beklenen değeri, aşağıdaki formül ile verilmektedir : Burada Pr {ε} olay ε ’nin meydana gelme olasılığını gösterir. Bu kitaptaki tüm örneklerde, bu olasılıklar yalnızca rasgeleleştirilmiş veri yapısı tarafından yapılan rasgele seçimler hakkındadır; ne veri yapısında depolanan verinin, ne de veri yapısı üzerinde gerçekleştirilen işlemlerin sırasının, rasgele olduğuna dair bir varsayım yoktur. Beklenen değerlerin en önemli özelliklerinden biri, beklenti doğrusallığıdır. Herhangi iki rasgele değişken X ve Y, için Daha genel olarak, herhangi rasgele değişkenler X1, … , Xk, için 24 Beklentinin doğrusallığı karmaşık rasgele değişkenleri (yukarıdaki denklemlerin sol tarafındakiler gibi) basit rasgele değişkenlerin toplamları halinde (sağ taraflar) bölmemiz için bize olanak tanır. Tekrar tekrar kullanacağımız yararlı bir hile, gösterge rasgele değişkenlerini tanımlamaktır. Bu ikili değişkenler bir şeyi saymak istediğimizde yararlıdır ve en iyi bir örnekle açıklanır. Adil bir bozuk parayı k kere fırlattığımızı varsayalım, para yere düşünce kaç kez tura gelmesinin beklenen sayısını bilmek istiyorum. Sezgisel olarak, cevabın k/2 olduğunu biliyoruz, ama eğer beklenen değer tanımını kullanarak bunu kanıtlamak için çalışırsak, Burada hesaplamayı ve ve ikili özdeşliklerini yeterince bilmemiz gereklidir. Gösterge değişkenlerini ve beklenti doğrusallığını kullanarak işler çok daha kolay hale gelir. Her i ∈ {1, …, k} için, gösterge rasgele değişkeni tanımlayalım : O zaman 25 Şimdi, , bu yüzden Bu, biraz daha uzun solukludur, ama bizim herhangi bir sihirli özdeşlik bilmemizi veya herhangi bir önemsiz olmayan olasılık hesaplamamızı gerektirmiyor. Daha da iyisi, paraların tam olarak yarısının tura geleceği sezgisi ile uyumludur., çünkü her para 1/2 olasılık ile tura olarak ortaya çıkıyor. 1.4 Hesaplama Modeli Bu kitapta, biz çalıştığımız veri yapıları üzerindeki işlemlerin teorik çalışma sürelerini analiz edeceğiz. Tam olarak bunu yapmak için, bir matematiksel hesaplama modeli gereklidir. Bunun için, biz w-bit sözcük-RAM modelini kullanacağız. RAM Rasgele Erişimli Makine’ye denk gelir. Bu modelde, her biri bir w-bit sözcük depolayan hücrelerden oluşan bir rasgele erişimli belleğe erişimimiz vardır. Bu demektir ki bir bellek hücresi, örneğin, {0, …, 2w − 1} kümesinden bir tamsayıyı simgeleyebilir. Sözcük-RAM modelinde, sözcükler üzerindeki temel işlemler sabit zaman alır. Bu aritmetik işlemleri (+, −, ∗, /, %), karşılaştırmaları (<, >, =, ≤, ≥), ve bit-bit-Boole işlemleri içerir (bitbit-VE, VEYA, ve dışlayıcı-VEYA). Herhangi bir hücre sabit zamanda okunabilir veya yazılabilir. Bir bilgisayarın belleği istediğimiz herhangi büyüklükte bir bellek bloğunun tahsis edilmesini veya serbest bırakılmasını sağlayacak bir bellek yönetim sistemi tarafından yönetilmektedir. k boyutunda 26 bir bellek bloğunu tahsis etmek O (k) zaman alır ve yeni ayrılan bellek bloğu için bir işaretçi döndürür. Bu işaretçi, bir tek sözcük ile temsil edilebilecek kadar küçüktür. Sözcük-boyutu, w, bu modelin çok önemli bir parametresidir. w hakkında yapılacak tek varsayım alt sınır w ≥ log n, burada n herhangi bir veri yapımızda saklanan elemanların sayısıdır. Bu oldukça orta halli bir varsayımdır, aksi takdirde bir veri yapısında saklanan elemanların sayısını saymak için bile sözcük yeterince büyük olmayacaktır. Alan sözcüklerle ölçülür, o yüzden bir veri yapısı tarafından kullanılan alan miktarından bahsederken, yapı tarafından kullanılan bellek sözcük sayısı ile ilgileniyoruz. Veri yapılarımızın tümü, bir genel tür olan T değerlerini saklar, ve T türü bir elemanın belleğin bir sözcüğünü kapladığını varsayarız. (Gerçekte, biz T türü nesnelere başvuruları depoluyoruz, ve bu referanslar bellekte sadece bir sözcük işgal ediyor.) w-bit sözcük-RAM modeli (32-bit) Java Sanal Makinesi (JVM) ile, w = 32 olduğunda, oldukça yakın uyuşur. Bu kitapta sunulan veri yapıları JVM ve çoğu diğer mimariler üzerinde uygulanabilir olmayan özel hileler kullanmaz. 1.5 Doğruluk, Zaman Karmaşıklığı ve Alan Karmaşıklığı Bir veri yapısının performansını incelerken, en önemli üç şey vardır : Doğruluk: Veri yapısı doğru şekilde arayüzünü uygulamalıdır. Zaman karmaşıklığı: Veri yapısı üzerindeki işlemlerin çalışma süreleri mümkün olduğunca küçük olmalıdır. Alan karmaşıklığı: Veri yapısı mümkün olduğunca az bellek kullanmalıdır. Bu giriş bölümünde, doğruluğu bize verilmiş olarak varsayacağız; sorgulara hatalı yanıtlar veren veya düzgün güncelleme yapmayan veri yapılarını dikkate almayacağız. Ancak, en az alan kullanımını çalıştırmak için ekstra bir çaba gösteren veri yapılarını göreceksiniz. Bu 27 genellikle işlemlerin (asimptotik) yürütme sürelerini etkilemez, ama pratikte veri yapılarını biraz daha yavaş yapabilir. Çalışma sürelerini veri yapıları bağlamında incelerken, üç farklı tür çalışma süresi garantisi eğilimi ile karşılaşırız: En-kötü durum çalışma süresi : Bunlar çalışma süresi garantilerinin en güçlü türüdür. Bir veri yapısı işlemi f (n) en-kötü durum çalışma süresine sahipse, o zaman her bir işlemin çalışma süresi f (n) den daha uzun zaman asla almaz. Amortize çalışma süresi : Bir veri yapısı içinde bir işlemin amortize çalışma süresinin f (n) olduğunu söylemek ise o zaman bu, tipik bir işlemin maliyetinin en fazla f (n) olduğu anlamına gelir. Daha kesin olarak, bir veri yapısı f (n) amortize çalışma süresine sahipse, bir dizi m işlemleri en fazla m f (n) zamanda çalışır. Bazı işlemler bireysel olarak f (n) den daha fazla sürebilir, ancak işlemlerin bütün dizisi için, ortalama, en fazla f (n) dir. Beklenen çalışma süresi: Bir veri yapısı üzerinde bir işlemin beklenen çalışma süresinin f (n) olduğunu söylediğimizde, bu gerçek çalışma süresinin bir rasgele değişken (bkz. Bölüm 1.3.4) olduğu ve bu rasgele değişkenin beklenen değerinin en fazla f (n) olduğu anlamına gelir. Buradaki rasgelelik veri yapısı tarafından yapılan rasgele seçimler ile ilgilidir. En-kötü durum, amortize ve beklenen çalışma zamanları arasındaki farkı anlamak için bir mali örneği düşünmek yardımcı olur. Bir ev satın alma maliyetini düşünün: En-kötü durum’a karşılık amortize maliyet : Bir ev maliyetinin 120,000 $ olduğunu varsayalım. Bu evi satın almak için, 1,200 $ aylık ödemeler ile 120 aylık (10 yıl) ipotek alabiliriz. Bu durumda, bu ipoteğin ödeme maliyeti en-kötü durumda aylık 1,200 $ olur. Elimizde yeterli para varsa, 120, 000 $ bir ödeme ile evi doğrudan satın almayı seçebiliriz. Bu durumda, 10 yıllık bir süre içinde, bu evi satın almanın amortize edilmiş aylık maliyeti 28 120,000 $ / 120 ay = 1,000 $ aylık . Bu, eğer ipoteği eğer ödemek zorunda kalsaydık ayda 1,200 $ ‘dan daha azdır. En-kötü durum’a karşılık beklenen maliyet : Şimdi, bizim 120,000 $’lık evimizde, yangın sigortası sorununu dikkate alalım. Yüzbinlerce olayı inceleyen sigorta şirketleri, bizimki gibi bir evde beklenen aylık yangın hasar miktarının 10 $ olduğunu belirlediler. Bu çok küçük bir sayıdır, çünkü pek çok evde asla yangın çıkmaz, birkaç evde biraz duman hasarı yaratan bazı küçük yangınlar olabilir, ve evlerin pek azı büyük yangın geçirir. Bu bilgilere dayanarak, sigorta şirketi yangın sigortası için 15 $ aylık ücret talep eder. Şimdi karar zamanı. Yangın sigortası için en-kötü durumda 15 $ aylık maliyeti ödemeli miyiz, ya da risk alıp ayda 10 $ beklenen maliyet pahasına kendi kendimizi sigortalamamız mı gerekir? Açıkçası, beklentide aylık 10 $ daha az masraf yapıyor, ama biz gerçek maliyetin çok daha yüksek olabileceği olasılığını kabul etmek zorundayız. Bütün evin yanması olası durumunda, gerçek maliyet 120,000 $ olacaktır. Bu finansal örnekler neden bazen en-kötü durumda çalışan zaman yerine amortize edilmiş veya beklenen çalışma süresi ile yetindiğimize dair bize ışık tutuyor. Çoğunlukla en-kötü zamandan daha düşük bir beklenen veya amortize zaman elde etmek mümkündür. En azından, amortize veya beklenen çalışma süreleriyle yetinmeye istekli bir kimsenin çok daha basit bir veri yapısı elde etmesi sıklıkla mümkündür. 1.6 Kod Örnekleri Bu kitaptaki kod örnekleri Java programlama dilinde yazılmıştır. Ancak Java'nın yapıları ve anahtar kelimelerinin tümüne aşina olmayan okuyuculara kitabı erişilebilir hale getirmek için, kod örnekleri basitleştirilmiştir. Örneğin, bir okuyucu public, protected, private veya static gibi anahtar kelimelerden hiçbirini bulamayacaktır. Okuyucu aynı zamanda sınıf hiyerarşileri hakkında da fazla tartışma bulamayacaktır. Belirli bir sınıfın hangi arayüzleri uyguladığı, veya hangi sınıfı genişlettiği, eğer tartışma ile ilgiliyse, ilişikteki metinden açık olmalıdır. Böylece, 29 B, C, C + +, C #, Objective-C, D, Java, JavaScript, dahil ALGOL geleneğinden gelen herhangi bir dilin birikimine sahip olan herkes tarafından kod örnekleri anlaşılabilir hale gelmiştir. Tüm uygulamaların bütün ayrıntılarını isteyen okuyucular bu kitaba eşlik eden Java kaynak koduna bakabilirler. Bu kitapta çalışma zamanlarının matematiksel analizleri ile analiz edilen algoritmalar için Java kaynak kodunu karışık halde bulacaksınız. Bunun anlamı, bazı denklemler kaynak kodunda da bulunan değişkenleri içerir. Bu değişkenler hem kaynak kodu içinde ve hem de denklem içinde sürekli dizilirler. Bu tür değişkenlerin en yaygını, istisnasız olarak, her zaman veri yapısında depolanan eleman sayısına karşılık gelen n ’dir. 1.7 Veri Yapıları Listesi Tablo 1.1 ve 1.2 Bölüm 1.2 'de anlatılan Liste, UKüme ve SKüme arayüzlerini bu kitapta gerçekleştiren veri yapılarının performansını özetliyor. 30 1.8 Tartışma ve Alıştırmalar Bölüm 1.2 ’de açıklanan Liste, UKüme ve SKüme arayüzleri Java Koleksiyonları Çerçevesi [54] tarafından etkilenmektedir. Bunlar Java Koleksiyonlar Çerçevesinde bulunan List, Set, Map, SortedSet ve SortedMap arayüzlerinin esas olarak basitleştirilmiş versiyonlarıdır. İlişikteki kaynak kod, Set, Map, SortedSet ve SortedMap uygulamaları içine UKüme ve SKüme uygulamalarını gerçekleştirmek için gerekli sarmalayan sınıfları içerir. Bu bölümde tartışılan matematik, asimptotik gösterim, logaritma, faktöriyel, Stirling yaklaşımı, temel olasılık ve daha fazlası dahil olmak üzere mükemmel (ve serbest) bir şekilde Leyman, Leighton, ve Meyer [50] tarafından ele alınmıştır. Üssel ve logaritmaların biçimsel tanımlarını içeren ılımlı bir hesap kitabı için ise, Thompson [73] tarafından kaleme alınmış (serbestçe kullanılabilir) klasik kitaba başvurunuz. 31 Özellikle bilgisayar bilimleri ile ilgili olarak temel olasılık hakkında daha fazla bilgi için, Ross [65] tarafından yazılmış ders kitabına bakmanız önerilir. Asimptotik gösterim ve olasılığı kapsayan başka bir iyi referans da, Graham, Knuth ve Patashnik tarafından yazılan ders kitabıdır [37]. Kendi Java programlamalarını tazelemek isteyen okuyucular etkileşimli pek çok Java eğitimlerini bulabilirler [56]. Alıştırma 1.1. Bu egzersiz doğru problem için doğru veri yapısı seçimini okuyucuya tanıtmaya yardımcı olmak için tasarlanmıştır. Gerçekleştirilirse, bu alıştırmanın bölümleri Java Koleksiyonları Çerçevesi tarafından sağlanan ilgili arayüzün (Yığıt, Kuyruk, İki Başlı Kuyruk, UKüme veya SKüme) bir uygulamasından yararlanarak yapılmalıdır. Aşağıdaki problemleri bir metin dosyasını her seferde bir satır okuyarak ve her satır için işlemleri uygun veri yapısı (ları) içinde gerçekleştirerek çözün. Uygulamalarınız, bir milyon satır içeren dosyaları bile birkaç saniye içinde işlenebilecek şekilde yeterince hızlı olmalıdır. 1. Girdiyi bir seferde bir satır olmak üzere okuyun ve daha sonra ters sırayla satırları yazın. Son giriş satırı ilk olarak yazılacaktır; daha sonra sondan ikinci giriş satırı yazılacaktır, ve benzeri. 2. Girdinin ilk 50 satırını okuyun ve daha sonra bunları ters sırayla yazın. Sonraki 50 satırı okuyun ve daha sonra ters sırayla bunları yazın. Bunu okumak için artık satır kalmayana dek devam ettirin. Bu noktada kalan satırlar tersten çıktıya yazılmalıdır. Diğer bir deyişle, çıktınız ilk satırda 50. satır, sonra 49.satır, sonra 48.satır ile başlayacak ve bu böyle 1.satıra kadar devam edecektir. Bunu takiben, 100.satır, sonra 99.satır yazılacak ve bu böyle 51.satıra kadar devam edecektir. Ve benzeri. Kodunuz herhangi bir zamanda asla 50 satırdan fazla saklamak zorunda değildir. 3. Her seferde bir girdi satırı okuyun. İlk 42 satırı okuduktan sonra herhangi bir noktada, eğer bazı satırlar boşsa (yani, 0 uzunluğunda bir dize) ondan 42 satır öncesindeki satırı çıktı olarak yazın. Örneğin 242.satır boş ise, programın çıktısı 200.satır olmalıdır. Bu 32 program herhangi bir zamanda, 43 satırdan fazla girdi depolamayacak şekilde gerçekleştirilmelidir. 4. Her seferde bir girdi satırı okuyun ve her satırı, önceki giriş satırlarında yinelenmediyse çıktıya yazın. Çok fazla yinelenen satır içeren bir dosyanın tekil satır sayısı için gerekli olandan daha fazla bellek kullanmaması gerektiğine özellikle dikkat edin. 5. Her seferde bir girdi satırı okuyun ve bu satırı daha önce zaten okuduysanız çıktıya yazın. (Sonuçta, her satır ilk geçtiği yerden silinecektir.) Çok fazla yinelenen satır içeren bir dosyanın tekil satır sayısı için gerekli olandan daha fazla bellek kullanmaması gerektiğine özellikle dikkat edin. 6. Tüm girdiyi her seferde bir satır olacak şekilde okuyun. Sonra uzunluğuna göre sıralanmış tüm satırları, kısa satırlar önce olmak üzere, çıktıya yazın. İki satırın aynı uzunluğa sahip olması durumunda, her zamanki “sıralama ölçütünü” kullanarak sıralarını çözümleyiniz. Yinelenen satırlar sadece bir kez yazılmalıdır. 7. Önceki soruda istenenin aynısını yapın, yalnız bu sefer yinelenen satırlar girdide göründükleri satır sayısı kadar çıktıda yazılmış olmalıdır. 8. Her seferde bir girdi satırı okuyun ve çift sayılı satırları (0.satır ile başlayan ilk satır ile) çıktıya yazın; daha sonra tek sayılı satırları yazın. 9. Tüm girdiyi her seferde bir satır olacak şekilde okuyun ve satırları çıktıya yazmadan önce rasgele permütasyon yapın. Açık olmak gerekirse: Herhangi bir satırın içeriğini değiştirmemeniz gerekir. Bunun yerine, aynı satır topluluğu yazdırılır, ama rasgele sırayla yapılmalıdır. Alıştırma 1.2. Bir Dyck sözcüğü +1 ve −1’lerden oluşur ve dizinin herhangi bir önek toplamının negatif olmaması şartı aranır. Örneğin +1, −1, +1, −1 bir Dyck sözcüğüdür, ancak +1, −1, −1, +1 değildir, çünkü önek +1 −1 – 1 < 0. Dyck sözcüğü ve Yığıt yığ (x) ve sil () işlemleri arasındaki herhangi bir ilişkiyi açıklayın. 33 Alıştırma 1.3. Bir uyumlu dize düzgün eşleştirilen bir {, }, (, ), [, ] karakter dizisidir. Örneğin, “{{()[]}}” bir uyumlu dizedir, ancak “{{()]}” değildir, çünkü ikinci { eşleşmesi ] olmuştur. n uzunluğunda bir dize verildiğinde, bunun bir uyumlu dize olup olmadığını belirlemek için O(n) zamanında çalışan bir yığıtın nasıl kullanılacağını gösterin. Alıştırma 1.4. Sadece yığ (x) ve sil () işlemlerini destekleyen bir Yığıt, s, olduğunu varsayalım. Sadece bir FIFO Kuyruk, q, kullanarak nasıl tüm elemanların sırasını tersine çevirebileceğinizi gösterin. Alıştırma 1.5. Bir UKüme kullanarak, Torba uygulayınız. Bir Torba UKüme gibidir − ekle(x), sil(x) ve bul(x) yöntemlerini destekler − ama yinelenen elemanların saklanmasını da sağlar. Bir Torba’daki bul (x) işlemi herhangi x elemanı (varsa) döndürür. Buna ek olarak, Torba x’e eşit olan tüm elemanların bir listesini döndüren hepsiniBul (x) işlemini destekler. Alıştırma 1.6. Liste, UKüme ve SKüme arayüzlerinin uygulamalarını sıfırdan yazınız ve test ediniz. Bunların verimli olması gerekmez. Daha verimli uygulamalarının doğruluğunu ve performansını test etmek için daha sonra kullanılabilir. (Bunu yapmanın en kolay yolu, elemanları bir dizide depolamaktır.) Alıştırma 1.7. Önceki soruda yaptığınız uygulamalarınızın performansını artırmak için aklınıza gelebilecek herhangi bir hileyi kullanarak çalışın. Liste uygulamasında ekle (i, x) ve sil (i) yöntemlerinin performansını nasıl artıracağınızı düşünün ve deney yapın. UKüme ve SKüme uygulamalarında bul (x) işleminin performansını nasıl artıracağınızı düşünün. Bu alıştırma size bu arayüzlerin etkili uygulamalarını elde etmenin ne kadar zor olabileceğine dair bir fikir vermek için tasarlanmıştır. 34 Bölüm 2 Dizi-Tabanlı Listeler Bu bölümde, Liste ve Kuyruk arayüzlerinin destek dizi denilen bir dizide temel verinin depolandığı uygulamalarını çalışacağız. Aşağıdaki tablo, bu bölümde sunulan veri yapıları için işlemlerin çalışma sürelerini özetliyor. Tek bir diziye veri depolayarak çalışan veri yapılarının ortak birçok avantaj ve sınırlamaları vardır : • Diziler dizideki herhangi bir değere sabit zamanlı erişimi sunuyor. Bu, al(i) ve belirle(i, x) • işlemlerinin sabit zamanda çalışmasını sağlıyor. Diziler çok dinamik değildir. Listenin ortasına yakın bir elemanın eklenmesi veya kaldırılması dizideki çok sayıda elemanın yeni eklenen elemana yer açmak için veya silinen eleman tarafından yaratılan boşluğu doldurmak için kaydırılmış olmasını gerektirir. Bu nedenle ekle (i, x) ve sil (i) işlemlerinin çalışma süreleri n ve i’ye bağlıdır. • Diziler genişlemez veya küçülmezler. Veri yapısındaki elemanların sayısı destek dizinin boyutunu aşarsa yeni bir dizinin tahsis edilmesi ve eski diziden verilerin yeni diziye kopyalanması gerekir. Bu pahalı bir işlemdir. Üçüncü nokta önemlidir. Yukarıdaki tabloda belirtilen çalışma süreleri büyüyen ve küçülen destek dizi ile ilişkili maliyeti dahil etmemektedir. Göreceksiniz ki, eğer dikkatli bir şekilde yönetilirse, destek diziyi büyültüp ve küçültmenin maliyeti ortalama bir işlemin maliyetine fazla bir yük getirmemektedir. Daha doğrusu, boş bir veri yapısı ile başlayarak herhangi bir sırada m adet ekle (i, x) ve sil (i) işlemini yürütürsek, destek diziyi büyültüp ve küçültmenin 35 tüm bu m işlem serisi üzerindeki maliyeti O(m) olur. Bazı bireysel işlemler daha pahalı olmasına rağmen tüm m adet işlem üzerinden amortize maliyet, her işlem için sadece O(1) 'dir. 2.1 Dizi Yığıtı: Dizi Kullanan Hızlı Yığıt İşlemleri Bir Dizi Yığıtı destek dizi denilen bir a dizisini kullanarak liste arayüzünü gerçekleştirir. i konumunda bulunan liste elemanı, a[i] içinde depolanır. Çoğu zaman, a, tam olarak gerekli olduğundan daha büyük olabilir, bu nedenle gerçekte a dizisinde saklanan elemanların sayısını tutmak için bir n tamsayısı kullanılır. Bu şekilde, listenin elemanları a[0] ,…, a[n-1] içine depolanır, ve her zaman a.boyut ≥ n olur. 2.1.1 Temel Bilgiler al(i) ve belirle(i, x) işlemlerini kullanarak Dizi Yığıtı elemanlarına erişmek ve onları değiştirmek kolaydır. Gerekli sınır denetimini yaptıktan sonra geriye sadece, sırasıyla, a[i] değeri döndürmek veya değiştirmek kalır. 36 Dizi Yığıtı’na eleman ekleme ve kaldırma işlemleri Şekil 2.1 ’de örneklenmiştir. ekle(i,x) işlemini gerçekleştirmek için ilk olarak a dizisinin halen dolu olup olmadığını kontrol ediyoruz. Eğer öyleyse, a ’nın boyutunu artırmak için yeniden_boyutlandır () yöntemini çağırıyoruz. yeniden_boyutlandır () işleminin nasıl gerçekleştirildiği daha sonra ele alınacaktır. Şimdilik, şu kadarını bilmek bize yeter, bir yeniden_boyutlandır () çağrısından sonra, emin olabiliriz ki a.boyut > n olur. Bunu tamamlayıp hallettikten sonra şimdi x ’e yer açmak için a[i],…,a[n − 1] elemanlarını bir konum sağa kaydırıyoruz, sonra a[i] elemanını x değerine eşitliyoruz ve n değerini 1 artırıyoruz. yeniden boyutlandır() çağrısının potansiyel maliyetini göz ardı edersek, ekle(i, x) işleminin maliyeti x’e yer açmak için kaydırmamız gereken elemanların sayısı ile orantılıdır. Bu nedenle, bu işlemin maliyeti (boyutlandırma maliyeti görmezden gelinerek) O(n–i+1) olur. sil (i) işlemini uygulamak da benzer şekildedir. a[i + 1],…,a[n − 1] elemanlarını bir konum sola kaydırıyoruz (a [i] üzerine yazarak), ve n değerini 1 azaltıyoruz. Bunu yaptıktan sonra a.length ≥ 3n koşulunu test ederek n değerinin a.length ’ten azımsanmayacak kadar çok küçük olup olmadığını kontrol ediyoruz. Koşul doğruysa, o zaman a’nın boyutunu azaltmak için yeniden_boyutlandır () işlemini çağırıyoruz. 37 yeniden_boyutlandır() yönteminin maliyetini göz ardı edersek, bir sil (i) işleminin maliyeti kaydırdığımız elemanların sayısı ile orantılıdır, yani O(n − i) ile ölçülür. 2.1.2 Büyültme ve Küçültme yeniden_boyutlandır() yöntemi oldukça basittir; boyutu 2n olan yeni bir b dizisi tahsis eder ve b’nin ilk n pozisyonuna n elemanı kopyalar ve daha sonra a’yı b’ye belirler. Böylece bir yeniden_boyutlandır() çağrısından sonra a.length = 2n olur. 38 yeniden_boyutlandır() yönteminin gerçek maliyetini analiz etmek kolaydır. 2n boyutlu bir b dizisi tahsis eder ve n elemanı b içine kopyalar. Bu O(n) zaman alır. Önceki bölümde yaptığımız çalışma süresi analizi yeniden boyutlandırmak için yapılan çağrıların maliyetini göz ardı etmişti. Bu bölümde amortize analiz olarak bilinen bir teknik kullanarak bu maliyeti analiz edeceğiz. Bu teknik her ekle(i, x) ve sil (i) işlemi sırasında gerçekleştirilen yeniden boyutlandırma maliyetini belirlemeyi denemiyor. Bunun yerine, ekle(i, x) ve sil (i) işlemlerinin m defa çağrılması sırasında yeniden boyutlandırmak için yapılan tüm çağrıların maliyetini düşünüyor. Özellikle, şunu göstereceğiz : Öneri 2.1: Boş bir Dizi Liste oluşturulursa ve ekle(i, x) ve sil (i) işlemleri seri olarak m ≥ 1 kere çağrılırsa, o zaman yeniden_boyutlandır () için yapılan tüm çağrılar sırasında harcanan toplam zaman O(m) olur. Kanıt: yeniden_boyutlandır () her çağrıldığında yeniden_boyutlandır () için yapılan son çağrıdan bu yana ekle veya sil için yapılan çağrı sayısının en az n/2 − 1 olduğunu göstereceğiz. Bu nedenle, yeniden_boyutlandır () için yapılan i'inci çağrı sırasındaki n değeri ni ile temsil edilirse ve yeniden_boyutlandır () için yapılan çağrıların toplam sayısı r ile gösteriliyorsa, o zaman ekle(i, x) ya da sil (i) için yapılan toplam çağrı sayısı en az 39 bu da şuna eşittir, Öte yandan, yeniden_boyutlandır () için yapılan tüm aramalar sırasında harcanan toplam zaman, r değeri m’den fazla olmadığından, Geriye yeniden_boyutlandır () için yapılan (i-1)’inci ve i’inci aramalar aralığında ekle (i, x) ya da sil (i) için yapılan çağrı sayısının en az ni / 2 olduğunu göstermek kalıyor. Dikkate alınması gereken iki durum vardır. İlk durumda, destek dizisi a dolu olduğundan ekle (i, x) tarafından yeniden_boyutlandır () çağrılıyor, yani, a.length = n = ni. Yeniden boyutlandırmak için yapılan bir önceki çağrıyı düşünün: Bir önceki bu çağrıdan sonra, a’nın uzunluğu a.length ve depolanmış elemanların sayısı en fazla a.length/2 = ni /2 olur. Fakat şimdi a içinde depolanmış elemanların sayısı ni = a.length, yani bir önceki yeniden_boyutlandır () çağrısından bu yana ekle (i, x) için en azından ni/2 çağrı yapılmıştır. İkinci durum yeniden_boyutlandır (), sil (i) tarafından çağrıldığında oluşur çünkü a.length ≥ 3n = 3ni. Yine, yeniden boyutlandırmak için yapılan bir önceki çağrıdan sonra a içinde depolanmış elemanların sayısı en az a.length/2 − 1 olur.3 Şimdi a içinde depolanmış ni ≤ a.length/3 adet eleman vardır. Bu nedenle, yeniden boyutlandırmak için yapılan en son çağrıdan bu yana sil (i) işlemlerinin sayısı en az 3 Bu formüldeki −1, n = 0 ve a.length = 1 iken oluşan özel durum için hesaplanmıştır. 40 Her iki durumda da, kanıtı tamamlamak için gerekli olduğu gibi, yeniden_boyutlandır () için yapılan (i-1)’inci ve i’inci aramalar aralığında ekle (i, x) ya da sil (i) için yapılan çağrı sayısı en az ni / 2 − 1 olmuştur. 2.1.3 Özet Aşağıdaki teorem Dizi Yığıt’ının performansını özetliyor : Teorem 2.1. Bir Dizi Yığıt, Liste arayüzünü uygular. yeniden_boyutlandır () için yapılan çağrıların maliyeti dikkate alınmayarak, Dizi Yığıt şu işlemleri destekler : • al(i) • ekle (i, x) ve belirle(i, x) işlem başına O(1) zamanı vardır; ve sil (i) işlem başına O(1+n−i) zamanı vardır. Bundan başka, boş bir Dizi Yığıt oluşturulursa ve ekle(i, x) ve sil (i) işlemleri m ≥ 1 defa çağrılırsa, yeniden_boyutlandır () çağrıları için gerekli toplam çalışma süresi O(m) olur. Dizi Yığıt bir Yığıt uygulamanın etkili bir yoludur. Özellikle, yığ(x) yöntemini ekle(n, x) ile ve sil(x) yöntemini sil(n−1) ile gerçekleştirebiliriz. Bu işlemler O(1) amortize zamanda çalışır. 2.2 Hızlı Dizi Yığıtı: Optimize Dizi Yığıtı Bir Dizi Yığıtı tarafından yapılan çalışmaların çoğu veri kaydırma yı (ekle(n, x) ve sil(x) tarafından) ve kopyalamayı (yeniden_boyutlandır () tarafından) içerir. Yukarıda gösterilen uygulamalarda, for döngüleri kullanarak bunu gerçekleştirdik. Oysa ki, birçok programlama ortamlarının veri bloklarını kopyalama ve taşıma konusunda çok verimli olan özel işlevlere sahip olduğu ortaya çıkmıştır. C programlama dilinde memcpy(d,s,n) ve memmove(d,s,n) fonksiyonları vardır. C++ içinde std :: copy(a0, a1, b) ve algoritması vardır. Java’da System:arraycopy (s, i, d, j, n) vardır. 41 Bu fonksiyonlar genellikle oldukça optimize edilmiştir ve hatta for döngüsü kullanarak yapabileceğimiz kopyalamadan çok daha hızlı özel makine komutları kullanırlar. Bu işlevleri kullanarak asimptotik çalışma saatlerini azaltmak mümkün olmasa da hala değerli birer optimizasyon olabilir. Buradaki Java uygulamalarında, sisteme özgü System.arraycopy(s,i,d,j,n) kullanımı, yapılan işlemlerin tiplerine bağlı olarak 2 ve 3 faktör arasında bir hız artışı ile sonuçlanmıştır. Sizin kilometre hızınız değişebilir. 2.3 Dizi Kuyruğu : Dizi-Tabanlı Kuyruk Bu bölümde, bir FIFO (ilk-giren-ilk-çıkar) kuyruğunu uygulayan Dizi Kuyruğu veri yapısını sunuyoruz, elemanlar eklendikleri (ekle () işlemi ile) sırayla aynı sırada kaldırılır (sil () işlemi ile). Bir Dizi Kuyruğu’nun bir FIFO kuyruğunu uygulamak için kötü bir seçim olduğuna dikkat edin. İyi bir seçim değildir, çünkü elemanları bunun üzerine eklemek için önce listenin bir ucunu seçmeliyiz ve daha sonra diğer ucundan elemanları kaldırmalıyız. İki işlemden birinin listenin başında çalışması gereklidir, ki burada i = 0 değerindeyken ekle (x, i) ya da sil (i) çağrıları yapılsın. Bu n orantılı çalışma süresi demektir. 42 Verimli bir dizi-tabanlı uygulamayı edinmek için öncelikle bir sonsuz dizi a olsaydı sorunun kolay olacağını fark etmeliyiz. Kaldırılacak bir sonraki elemanın konumunu tutan bir j endeksi ve kuyruktaki eleman sayısını sayan bir tamsayı n tutardık. Kuyruk elemanları her zaman için şöyle saklanacaktır : Başlangıçta, j ve n ikisi de 0'a ayarlanmış olacaktır. Bir elemanı eklemek için a[j + n] konumuna yerleştiririz ve n değerini 1 artırırız. Bir elemanı kaldırmak için, a[j] konumundan onu sileriz, j değerini 1 artırırız ve n değerini 1 azaltırız. Tabii ki, bu çözümdeki sorun, sonsuz bir dizi gerektirmesidir. Bir Dizi Yığıtı sonlu bir dizi a ve modüler aritmetik kullanarak bu çözüm yoluna benzetim yapar. Bu, günün zamanı hakkında konuşurken kullanılan aritmetik türüdür. Örneğin 10:00 artı beş saat 03:00 verir. Biçimsel olarak demek istiyoruz ki, Bu denklemin ikinci bölümü “15 modülo 12 denktir 3.” şeklinde okunur. Bir ikili operatör olarak da mod ele alınabilir; şöyle ki Daha genel olarak, bir tamsayı a ve pozitif bir tamsayı m için, a mod m benzersiz bir r tamsayısına denktir öyle ki r ∈ {0 ,…, m 1} ve herhangi bir k tamsayısı için a = r + km eşitliği sağlanmalıdır. Daha az biçimsel olursak, r değeri a, m’e bölündüğünde ortaya çıkan kalandır. Java gibi birçok programlama dillerinde, mod operatörü % sembolü ile temsil edilir.4 4 İlk argüman negatif olduğunda bu doğru matematiksel mod operatörünü uygulamıyor. 43 Modüler aritmetik sonsuz bir diziye benzetim yapmak için yararlıdır, çünkü i mod a.length her zaman 0,…,a.length − 1 aralığında bir değer verir. Modüler aritmetik kullanarak kuyruk elemanlarını aşağıdaki dizi konumlarında saklayabiliriz: Burada a dizisi bir dairesel dizi gibi değerlendirilmektedir. a.length − 1’den daha büyük olan dizi endekslerinin dizinin başlangıcına doğru "sarar". Endişelenecek ve dikkat edilmesi gözetilecek tek kalan şey, Dizi Kuyruk elemanlarının sayısı a boyutunu geçmemelidir. Dizi Kuyruk üzerinde ekle (x) ve sil () işlemlerinin örnek bir yürütmesi Şekil 2.2 'de gösterilmiştir. ekle (x)’i gerçekleştirmek için öncelikle a’nın dolu olup olmadığı kontrol ederiz ve gerekirse, a’nın boyutunu artırmak için yeniden_boyutlandır () çağrısı yaparız. Sonra, x değerini a[(j + n) % a.length] içinde saklarız ve n değerini 1 artırırız. sil () işlemini gerçekleştirmek için öncelikle, daha sonra ona geri dönmek üzere a [j] elemanını saklarız. Sonra, n değerini 1 azaltırız ve j = (j + 1) mod a.length ayarlamasıyla j (modülo a.length) değerini 1 artırırız. Son olarak, a [j] konumunda önceden saklamış olduğumuz değeri döndürürüz. Gerekirse, a boyutunu azaltmak için yeniden_boyutlandır () da çağrılabilir. 44 45 Son olarak, yeniden_boyutlandır () işlemi Dizi Yığıt içinde çalışan yeniden boyutlandırmaya çok benzerdir. Boyutu 2n olan yeni bir b dizisi tahsis eder ve aşağıdaki elemanları şunların üzerine yazar : ve j = 0 olarak ayarlar. 2.3.1 Özet Aşağıdaki teorem Dizi Kuyruk veri yapısının performansını özetliyor : Teorem 2.2. Bir Dizi Kuyruk, (FIFO) Kuyruk arayüzünü uygular. yeniden_boyutlandır() için yapılan çağrıların maliyeti dikkate alınmayarak, ekle (x) ve sil () işlemleri Dizi Kuyruk tarafından işlem başına O(1) zamanda desteklenir. Ayrıca, boş bir Dizi Kuyruk ile başlandığında, ekle (i, x) ve sil (i) işlemleri toplam m defa çağrılırsa, tüm yeniden_boyutlandır () çağrıları için gerekli toplam çalışma süresi O(m) olur. 46 2.4 Dizi İki Başlı Kuyruk: Bir Dizi Kullanan Hızlı İki Başlı Kuyruk İşlemleri Daha önceki bölümlerde elde edilen Dizi Kuyruk dizinin bir ucuna verimli olarak eklenilmesi ve diğer ucundan silinmesini sağlayan bir kuyruğu temsil eden veri yapısıydı. Dizi İki Başlı Kuyruk veri yapısı, her iki uçta da etkili ekleme ve çıkarma yapmaya izin verir. Bu yapı, Dizi Kuyruk’u temsil etmek için kullanılan dairesel dizi tekniğinin aynısını kullanarak Liste arayüzünü uygular. Bir Dizi İki Başlı Kuyruk üzerinde çalışan al (i) ve belirle (i, x) işlemlerini gerçekleştirmek kolaydır. Bunlar a[(j + i) mod a.length] konumundaki dizi elemanını getirir veya o değer için atama yapar. 47 ekle (i, x) uygulaması biraz daha ilginçtir. Her zamanki gibi, ilk önce a’nın dolu olup olmadığını kontrol ediyoruz ve gerekirse, a’yı yeniden boyutlandırmak için yeniden_boyutlandır () çağrısı yapmalıyız. i küçük (0 civarında) olduğunda, ya da i büyük (n’e yakın) olduğunda, bu işlemin hızlı olmasını istediğimizi unutmayın. Bu nedenle, i < n/2 koşulunu kontrol ederiz. Eğer öyleyse, a[0],…,a[i − 1] elemanlarını sola doğru 1 konum kaydırırız. Aksi halde (i ≥ n/2) iken, a[i],…,a[n − 1] elemanlarını sağa doğru 1 konum kaydırırız. Dizi İki Başlı Kuyruk üzerinde çalışan ekle(i, x) ve sil (i) işlemlerine bir örnek için Şekil 2.3 ’e bakınız. 48 Bu şekilde kaydırma yaparak, ekle (i,x) işleminin min(i, n − i) elemandan fazlasını kaydırmayacağı garanti edilir. Bu nedenle, ekle (i, x) işleminin çalışma süresi (yeniden_boyutlandır () işleminin maliyeti göz ardı edildiğinde) O(1 + min {i, n − i}) olur. sil (i) işleminin uygulanması da benzer şekildedir. i < n2 koşuluna bağlı olarak ya a[0],…,a[i − 1] elemanlarını sağa doğru 1 konum kaydırılır, ya da a[i + 1],…,a[n − 1] elemanlarını sola doğru 1 konum kaydırılır. Yine bunun anlamı, sil (i) işleminin elemanları kaydırmak için asla O((1 + min {i, n − i}) zamandan daha fazla harcamayacağıdır. 2.4.1 Özet Aşağıdaki teorem Dizi İki Başlı Kuyruk veri yapısının performansını özetliyor: Teorem 2.3. Bir Dizi İki Başlı Kuyruk, Liste arayüzünü uygular. yeniden_boyutlandır () için yapılan çağrıların maliyeti dikkate alınmayarak, bir Dizi İki Başlı Kuyruk aşağıdaki işlemleri destekler : • al (i) • ekle (i, x) ve belirle (i, x) işlem başına O(1) zamanda gerçekleşir; ve ve sil (i) işlem başına O((1 + min {i, n − i}) zamanda gerçekleşir. 49 Ayrıca, boş bir Dizi İki Başlı Kuyruk ile başlandığında, ekle (i, x) ve sil (i) işlemleri toplam m defa çağrılırsa, tüm yeniden_boyutlandır () çağrıları için gerekli toplam çalışma süresi O(m) olur. 2.5. Çifte Dizi İki Başlı Kuyruk : İki Yığıt’tan Bir İki Başlı Kuyruk Oluşturulması Şimdi, bir Dizi İki Başlı Kuyruk ile aynı performans sınırını sağlayan, fakat iki adet Dizi Yığıt kullanan ve bu nedenle Çifte Dizi İki Başlı Kuyruk adı verilen bir veri yapısını sunuyoruz. Çifte Dizi İki Başlı Kuyruk’un asimptotik performansı Dizi İki Başlı Kuyruk’tan daha iyi olmamasına rağmen hala üzerinde düşünmeye ve incelemeye değerdir, çünkü iki basit veri yapısını birleştirerek gelişmiş bir veri yapısının nasıl yapıldığına dair iyi bir örnek sunmaktadır. Çifte Dizi İki Başlı Kuyruk iki adet Dizi Yığıt kullanarak bir listeyi temsil eder. Dizi Yığıt’ın listenin uçlarındaki elemanları değiştirdiği zaman hızlı olduğunu hatırlayın. Çifte Dizi İki Başlı Kuyruk front ve back adı verilen karşılıklı birer Dizi Yığıt yerleştirir, böylece işlemler çalıştığında her iki uçta da hızlı olacaktır. Çifte Dizi İki Başlı Kuyruk eleman sayısı olan n’i açıkça depolamaz. Buna gerek yoktur çünkü, eleman sayısı, n = front.boyut() + back.boyut() ile bulunur. Yine de, Çifte Dizi İki Başlı Kuyruk’u analiz ederken, içerdiği eleman sayısını göstermek için n değişkenini kullanmaya devam edeceğiz. 50 front Dizi Yığıt, endeksleri 0,…,front.boyut() − 1 olan liste elemanlarını saklar ama ters sırada saklar. back Dizi Yığıt, endeksleri front.boyut(), …,boyut() − 1 olan liste elemanlarını normal sırada saklar. Bu şekilde, al (i) ve belirle (i, x) işlemleri ön ya da arka üzerine yönlendirilerek işlem başına O(1) zamanda çalışacak olan uygun al (i) ve belirle (i, x) çağrılarına çevrilir. Herhangi bir i < front.boyut() koşulunu sağlayan i endeksine ait eleman, front.boyut() −i−1 konumundaki front elemanına karşılık gelir, çünkü front elemanlar tersten saklanmaktadır. 51 Çifte Dizi İki Başlı Kuyruk’a eleman ekleme ve çıkarma Şekil 2.4 'de gösterilmiştir. ekle (i, x) işlemi uygun olması gerekecek şekilde front ya da back yığıtı işler : ekle (i, x) denge () 2 yöntemi denge () yöntemini çağırarak front ve back Dizi Yığıt’larını dengeler. uygulanması aşağıda açıklanmıştır, ama şimdilik şunu bilmek yeterlidir: boyut() < olmadığı sürece, dengeleme sonucunda front.boyut() ve back.boyut() değerleri 3 kattan daha fazla farklılaşmazlar. denge () bize bunu garanti eder. Özellikle, 3 front.boyut() ≥ back.boyut() ve 3 back.boyut() ≥ front.boyut() yazılır. Şimdi, dengele () için yapılan çağrıların maliyetini görmezden gelerek, ekle (i, x) maliyetini analiz edeceğiz. Eğer i<front.boyut() ise, ekle(i, x) yöntemi front.ekle(front.boyut()−i−1,x) çağrısı tarafından gerçekleştirilir. front bir Dizi Yığıt olduğu için bunun maliyetini şöyle yazarız : Öte yandan, eğer i ≥ front.boyut() ise, ekle(i, x) yöntemi back.ekle(i − front.boyut(), x) çağrısı tarafından gerçekleştirilir. Bunun maliyeti, Farkına varmalısınız ki, birinci durum (2.1) i < n/4 koşulunda oluşur. İkinci durum (2.2) i ≥ 3n/4 koşulunda oluşur. n/4 ≤ i < 3n/4 koşulunda işlemin front ya da back yığıtlarından 52 hangisini etkilediğinden emin olamayız, fakat her iki durumda da, i ≥ n/4 ve n − i > n/4 olduğundan işlem O(n) = O(i) = O(n − i) zaman alır. Durumu özetlersek, Bu nedenle, ekle (i, x) çalışma süresi, denge () çağrısının maliyetini görmezden gelirsek, O(1 + min{i, n − i }) olur. sil (i) işlemi ve analizi, ekle (x, i) analizi ile benzeşmektedir. 2.5.1 Dengeleme Son olarak, ekle (i, x) ve sil (x) tarafından gerçekleştirilen dengele () işlemine bakalım. Bu işlem front ve back yığıtlarının ne çok büyük ne de çok küçük olmamasını garanti ediyor. İki elemandan daha az boyutta olmadıkları sürece, front ve back’in her birinin en az n/4 eleman içermesini sağlıyor. Bu durum geçerli değilse, o zaman elemanları aralarında yer değiştirerek hareket ettiriyor. Böylece front ve back tam olarak sırasıyla n/2 eleman ve n/2 eleman içeriyor. 53 Burada analiz edilmesi gereken çok az şey var. dengele () işlemi dengelemeyi yapıyorsa, o zaman O(n) elemanı bir konumdan başka bir konuma taşımıştır ve bu O(n) zaman alır. Bu kötüdür, çünkü ekle (i, x) ve sil (x) için yapılan her çağrıda dengele () çağrılır. Bununla birlikte, aşağıdaki öneri gösteriyor ki, ortalama olarak, dengele () sadece işlem başına sabit miktarda zaman harcamaktadır. Öneri 2.2. Boş bir Çifte Dizi İki Başlı Kuyruk oluşturulursa ve ekle(i, x) ve sil (i) işlemleri seri olarak m ≥ 1 kere çağrılırsa, o zaman dengele () için yapılan tüm çağrılar sırasında harcanan toplam zaman O(m) olur. Kanıt. Eğer dengele () elemanları kaydırmak zorunda kalırsa, son kaydırmadan bu yana çağrılmış bulunan ekle (i, x) ve sil (i) işlemlerinin sayısı en az n/2 – 1 olur. Öneri 2.1’in kanıtında olduğu gibi, dengele() tarafından harcanan toplam zamanın O(m) olduğunu kanıtlamak için bu yeterlidir. 54 Burada potansiyel yöntemi olarak adlandırılan bir teknik kullanarak analizimizi gerçekleştireceğiz. Çifte Dizi İki Başlı Kuyruk’un potansiyeli, Ф, front ve back boyutları arasındaki fark olarak tanımlanır. Bu potansiyel hakkında ilginç olan şey şudur ki, dengeleme yapmaya gerek duymayan bir ekle (i, x) ve sil (i) çağrısı potansiyeli en çok 1 artırabilir. Gözlemlemelisiniz ki, elemanları kaydıran bir dengele() çağrısından hemen sonra, potansiyel Ф0, en çok 1’dir, çünkü Elemanları kaydıran bir dengele () çağrısından hemen önceki durumu düşünün ve detayları atlayarak varsayalım ki, 3front.boyut() < back.boyut() olduğu için dengele () elemanları kaydırmaktadır. Bu durumda, farkında olmalısınız ki, Ayrıca, bu andaki potansiyel olur. 55 Bu nedenle, dengele () tarafından yapılan son kaydırmadan itibaren ekle (i, x) ve sil (i) işlemlerine yapılan çağrıların sayısı en az Ф1 − Ф0 > n/2 − 1 olur. Bu kanıtlamayı tamamlar. 2.5.2 Özet Aşağıdaki teorem bir Çifte Dizi İki Başlı Kuyruk’un özelliklerini özetlemektedir: Teorem 2.4. Bir Çifte Dizi İki Başlı Kuyruk, Liste arayüzünü uygular. yeniden_boyutlandır() ve dengele() için yapılan çağrıların maliyeti dikkate alınmayarak, bir Çifte Dizi İki Başlı Kuyruk aşağıdaki işlemleri destekler : • al (i) • ekle (i, x) ve belirle (i, x) işlem başına O(1) zamanda gerçekleşir; ve ve sil (i) işlem başına O((1 + min {i, n − i}) zamanda gerçekleşir. Ayrıca, boş bir Çifte Dizi İki Başlı Kuyruk ile başlandığında, ekle (i, x) ve sil (i) işlemleri toplam m defa çağrılırsa, tüm yeniden_boyutlandır () ve dengele () çağrıları için gerekli toplam çalışma süresi O(m) olur. 2.6 Kök Dizi Yığıt: Alan Kriteri Verimli Olan Bir Dizi Yığıt Uygulaması Bu bölümde anlatılan önceki veri yapılarının tümünde ortak sorun, kendi verilerini bir ya da iki dizide depolarken bu dizilerin sık sık boyutlandırılmaması ve çok sık dolu olmamasıdır. Örneğin, Dizi Yığıt üzerinde gerçekleştirilen bir yeniden_boyutlandır () işleminden hemen sonra destek dizisi olan a’nın sadece yarısı doludur. Daha da kötüsü, a’nın sadece 1/3 oranında veri içerdiği zamanlar vardır. 56 Bu bölümde, boşa harcanan alan sorununu ele alan Kök Dizi Yığıt veri yapısını tartışacağız. Kök Dizi Yığıt, n anda en çok elemanı diziyi kullanarak depolar. Bu diziler içinden, herhangi bir dizi konumu kullanılmamıştır. Kalan tüm dizi konumları veri depolamak için kullanılır. Bu nedenle, bu veri yapıları n elemanı saklarken en çok alan kaybetmiştir. Kök Dizi Yığıt, elemanlarını 0, 1, …, r − 1 olarak numaralandırılan ve blok adı verilen r adet diziden oluşan bir listede depolar. Şekil 2.5 'e bakınız. Blok b, b + 1 elemanı içerir. Bu nedenle, tüm r blokları toplam olarak eleman içerir. Yukarıdaki formül Şekil 2.6 'de gösterildiği gibi elde edilebilir. Tahmin edebileceğimiz gibi, listenin elemanları blok içinde sırayla ortaya konmuştur. Endeksi 0 olan liste elemanı, blok 0’da saklanır, endeksleri 1 ve 2 olan liste elemanları, blok 1’da depolanır, liste endeksleri 3, 4, 5 olan elemanlar blok 2’de depolanır, vb. Üzerinde durmamız gereken temel sorun, endeks i verildiğinde hangi bloğun i içerdiğini ve hem de bu blok içinde i’ye karşılık gelen endeksi belirlemektir. Kendi bloğu içinde i endeksini belirlemek kolaydır. i endeksi b bloğunun içinde ise o zaman blok 0, 1, …, b – 1 içinde bulunan elemanların sayısı b(b+1)/2 olur. Bu nedenle, i, b bloğu içinde konumunda saklanmıştır. Biraz daha zor olanı b değerini belirleme sorunudur. Endeksi i’den daha az ya da eşit olan elemanların sayısı i + 1’e eşittir. Diğer taraftan, 0, …, b bloklarındaki 57 elemanların sayısı (b+1)(b+2)/2’dir. Bu nedenle, b, koşulunu sağlayan en küçük tamsayıdır. Bu denklemi 58 olarak tekrar yazabiliriz. Buna karşılık gelen b2 + 3b – 2i = 0 ikinci dereceden denkleminin iki çözümü vardır : ve İkinci çözüm bizim uygulamamız için hiç mantıklı gelmiyor, çünkü her zaman negatif bir değer verir. Bu nedenle, çözümünü elde ederiz. Genel olarak, bu çözüm bir tam sayı değildir, ama eşitsizliğimize geri gidersek, biz koşulunu sağlayan en küçük tamsayı olan b’yi istiyoruz. Basitçe bu, olur. Bu şekilde al (i) ve belirle (i, x) yöntemlerini yapmak karmaşık değildir, anlaması kolaydır. İlk olarak, uygun b bloğunu ve blok içindeki uygun j endeksini hesaplarız ve daha sonra uygun işlemi gerçekleştiririz : 59 Blok listesini temsil etmek için bu bölümdeki veri yapılarından herhangi birini kullanmışsak, o zaman al (i) ve belirle (i, x) işlemlerinin her ikisi de sabit sürede çalışacaktır. ekle (i, x) yöntemi, artık, tanıdık gelecektir. Öncelikle blok sayısının r(r+1) / 2 = n eşitliğini sağlayıp sağlamadığını kontrol ettikten sonra, veri yapısının tam dolu olup olmadığını belirleriz. Burada r blok sayısıdır. Eğer eşitlik sağlanmışsa, başka bir blok eklemek için büyült () çağrısı yaparız. Bu işlem ile endeksi i olan yeni elemana yer açmak için i, …,n − 1 endeksli elemanları 1 konum sağa kaydırırız. büyült () yöntemi beklendiği gibi, yeni bir blok ekler : Büyültme işleminin maliyetini görmezden gelerek, bir ekle (i, x) işleminin maliyetinin en önemli parçası kaydırmanın maliyetidir ve bu yüzden bir Dizi Yığıt’daki gibi sadece O(1+n−i) zaman alır. sil (i) işlemi ekle (i, x) işlemine benzerdir. i+1, …,n endeksli elemanları 1 konum sola kaydırır, ve daha sonra, kullanılmayan blokların birini kaldırmak için küçült () yöntemini çağırır. 60 Bir kez daha, küçült () işleminin maliyeti görmezden gelinerek, sil (i) işlemine ait maliyetin en önemli parçası kaydırmanın maliyetidir ve bu nedenle O(n - i) çalışma süresi vardır. 2.6.1 Büyültme ve Küçültmenin Analizi ekle (i, x) ve sil (i) ile ilgili yaptığımız yukarıdaki analizde büyült () ve küçült () maliyetini hesaba katmamıştık. Unutmayın ki, Dizi Yığıt.yeniden_boyutlandır () yönteminin aksine, büyült () ve küçült () herhangi bir veri kopyalamaz. Sadece r boyutlu bir diziyi tahsis eder veya boşaltır, temizler. Bazı ortamlarda, bu sadece sabit zaman alırken, diğerlerinde ise, r orantılı zaman gerektirebilir. büyült () veya küçült () çağrısından hemen sonra durum açıktır. Son blok tamamen boştur ve diğer tüm bloklar tamamen doludur. En az r − 1 elemanlar eklenene veya çıkarılana kadar büyült () veya küçült () için başka bir çağrı yapılmayacaktır. Bu nedenle büyült() ve küçült() işlemleri O(r) zaman alsa bile bu maliyet en az r – 1 adet ekle (i, x) veya sil (i) işlemi üzerinden amortize edilebilir, böylece büyült () ve küçült ()’ün işlem başına amortize maliyeti O(1) olur. 61 2.6.2 Alan Kullanımı Şimdi, bir Kök Dizi Yığıt tarafından kullanılan ekstra alan miktarını analiz ediyoruz. Özellikle, bir Kök Dizi Yığıt tarafından kullanılan ve şu anda bir liste elemanını tutmak için kullanılmayan dizi elemanlarının kapladığı alanı saymak istiyoruz. Bu tür alana israf alanı diyoruz. sil (i) işlemi, bir Kök Dizi Yığıt’ın asla tamamen dolu olmayan iki bloktan daha fazlasına sahip olmayacağını garanti eder. Bu nedenle n elemanı saklayan bir Kök Dizi Yığıt tarafından kullanılan blok sayısı, r, koşulunu sağlar. Bunu ikinci dereceden denklem olarak çözersek, elde edilir. Son iki bloğun boyutları r ve r − 1’dir. Bu yüzden bu iki blok tarafından israf edilen alan en fazla olur. Biz, (örneğin) bir Dizi Liste’de blokları depoladığımız takdirde, o zaman r bloğu depolayan Liste tarafından israf edilen alan miktarı olur. n ve diğer sistematik kayıtları depolamak için gerekli alanlar O(1)’dir. Bu nedenle, bir Kök Dizi Yığıt’da israf edilen toplam alan miktarı olur. Sonra, bu alan kullanımının boş başlayan herhangi bir veri yapısı için uygun olduğunu ve her seferinde bir eleman eklenmesini destekleyebildiğini iddia ediyoruz. Daha kesin bir ifadeyle, n elemanın eklenmesi sırasında bir noktada, veri yapısının, en az miktarında bir boşluğu israf ettiğini göstereceğiz (gerçi sadece bir an için israf edilebilse de). Boş bir veri yapısı ile başladığımızı varsayalım ve her seferinde bir tane olmak üzere toplam n elemanı ekliyoruz. Bu işlemin sonunda, n elemanın tamamı veri yapısında depolanmış ve r bellek bloklarının arasına dağıtılmış olacaktır. Eğer koşulu doğru ise, o zaman veri yapısının bu r bloğu izlemek için r gösterge (veya referans) kullanıyor olması gerekir, ve bu göstergeler alanı israf edilmektedir. Öte yandan, 62 koşulu doğruysa, güvercin deliği ilkesi ile, bir blok en azından boyuta sahip olmalıdır. Bu bloğun ilk tahsis edildiği anı düşünün. Bu tahsisten hemen sonra, bu blok boştu, ve bu nedenle alanı israf ediyordu. Bu nedenle, n elemanların yerleştirilmesi sırasında zaman içinde bir noktada veri yapısı alanı israf ediyordu. 2.6.3 Özet Aşağıdaki teorem Kök Dizi Yığıt veri yapısı ile ilgili tartışmamızı özetliyor : Teorem 2.5. Bir Kök Dizi Yığıt, Liste arayüzünü uygular. büyült () ve küçült () çağrılarının maliyetini dikkate almayarak, Kök Dizi Yığıt • işlem başına O(1) zamanda çalışan al (i) ve belirle (i, x); ve • işlem başına O(1 + n − i) zamanda çalışan ekle (i, x) ve sil (i) işlemlerini destekler. Bundan başka, boş bir Kök Dizi Yığıt ile başlandığında, ekle (i, x) ve sil (i) işlemleri toplam m defa çağrılırsa, tüm büyült () ve küçült () çağrıları için gerekli toplam çalışma süresi O(m) olur. n elemanı depolayan bir Kök Dizi Yığıt tarafından kullanılan alan5 (sözcük cinsinden ölçüldüğünde) ’dir. 2.6.4 Karekökleri Hesaplamak Hesaplama modellerini biraz bilen bir okuyucu, fark etmiştir ki, yukarıda anlatıldığı gibi bir Kök Dizi Yığıt her zamanki sözcük-RAM hesaplama modeline uymuyor (Bölüm 1.4) çünkü bu karekök almayı gerektirir. Karekök işlemi genellikle bir temel işlem olarak kabul edilmez ve bu nedenle çoğu zaman sözcük-RAM modelinin bir parçası değildir. 5 Belleğin nasıl ölçüldüğünü Bölüm 1.4’teki tartışmadan hatırlayın. 63 Bu bölümde, karekök işleminin etkin bir şekilde uygulanabilir olduğunu göstereceğiz. Özellikle herhangi bir tamsayı x ∈ {0, …, n} için, önişlemesinden sonra uzunluğunda iki dizi yaratan ’in sabit bir sürede hesaplanabileceğini göstereceğiz. Aşağıdaki önerme göstermektedir ki x'in karekökünü hesaplama sorunu ilgili bir x’ değerinin kare köküne indirilebilir. Öneri 2.3. iken x ≥1 ve x’ = x − a olsun. O zaman olur. Kanıt. Şunun doğru olduğunu göstermek yeterlidir : Bu eşitsizlikte her iki tarafın karesi alındığında, elde ederiz. Terimleri bir araya getirdiğimizde, herhangi bir x ≥ 1 için açıkça doğru olan elde edilir. Problemi biraz daraltarak başlayın, ve varsayın ki 2r ≤ x ≤ 2 r+1 için log x = r, yani, x ikili gösterimi r + 1 bit olan bir tam sayıdır. Kabul edebiliriz ki, x’ = x − ( x mod 2 r / 2 ). Şimdi, x’ Öneri 2.3’ün koşullarını yerine getirmektedir, dolayısıyla . Ayrıca, x’ değerini oluşturan r/2 alt sıralı bitin hepsi 0’a eşittir, yani mümkün olabilen her x’ değeri için söylenebilir. Bu olası her x’ değeri için ’nun değerini depolayan sqrttab adında bir dizi kullanabileceğimiz anlamına gelir. Biraz daha doğrusunu söylemek gerekirse, 64 değerlerine sahibiz. Bu şekilde, sqrttab[i] her x ∈ { i2r/2 ,…,(i+1)2r/2 − 1 } için ’in 2 değeri ile sınırlanmıştır. Diğer bir deyişle, s = sqrttab[x>>r/2 ] girişindeki dizi elemanının değeri ya ya da olur. s yardımıyla değerini de belirleyebiliriz. Bunun için s değeri (s+1)2 > x olana kadar artırılır. Şimdi, bu sadece x ∈ { 2r,…, 2r+1 − 1 } için çalışır ve sqrttab sadece belirli bir r = log x değeri için çalışan özel bir tablo olmuştur. Bunun üstesinden gelmek için, olası her log x değeri için birer tane olacak şekilde log n adet farklı sqrttab dizisi hesaplayabiliriz. Bu tabloların boyutları incelendiğinde, en büyük değeri yani tüm tabloların toplam boyutu olan bir üstel dizi ile karşılaşılır, olur. Ancak, birden fazla sqrttab dizisinin gereksiz olduğu ortaya çıkar; sadece r = log n değeri için bir sqrttab dizisine gerek duyarız. log x = r’ < r denklemini sağlayan herhangi bir x değeri 2r-r’ ile çarpıldığında ve denklemi kullanılarak yükseltilebilir. 2 r-r’ x miktarı { 2r,…, 2r+1 − 1 } aralığındadır, bu yüzden onun karekökünü sqrttab içinde arayabiliriz. Aşağıdaki kod bu fikri, { 0,…, 230 − 1 } aralığındaki negatif olmayan tüm x tamsayıları için ve 216 büyüklüğünde bir sqrttab dizisi kullanarak, için gerçekleştirmiştir. 65 değerini hesaplamak Şimdiye kadar sorgusuz sualsiz doğru olduğunu varsaydığımız şey, r’= log x ’in nasıl hesaplanacağı sorusudur. Bu yine, 2r/2 boyutunda bir dizi, logtab, ile çözülebilecek bir sorundur. Bu durumda, kod, özellikle basittir, çünkü log x x’in ikili gösteriminde en önemli 1 bit’inin sadece endeksidir. Bu demektir ki, logtab dizininde bir endeks olarak kullanılmadan önce, x > 2r/2 için x bitlerini r/2 pozisyonu kadar sağa kaydırabiliriz. Aşağıdaki kod 216 büyüklüğünde bir logtab dizisini kullanarak { 0,…, 232 − 1 } aralığındaki tüm x için log x değerini hesaplamaktadır. Son olarak, eksiksiz olmak için, logtab ve sqrttab dizilerini ilk kullanıma hazırlayan aşağıdaki kodu ekliyoruz : 66 Özetlemek gerekirse, i2b (i) yöntemi ile yapılan hesaplamalar sözcük-RAM üzerinde sqrttab ve logtab dizilerini depolamak için gerekli olan ekstra bellek kullanarak sabit zamanda gerçekleştirilebilir. n iki kat arttığı veya azaldığı zaman bu diziler yeniden inşa edilebilir ve bu yeniden inşa maliyeti Dizi Yığıt uygulamasında analiz edilen yeniden_boyutlandır () maliyetiyle aynı şekilde n’de değişikliğe sebep olan ekle (i, x) ve sil (i) işlemlerinin sayısı üzerinden amortize edilebilir. 2.7 Tartışma ve Alıştırmalar Bu bölümde açıklanan veri yapılarının çoğunda 30 yıl geriye uzanan uygulamalarda rastladığımız benzerlik ve çeşitlilik bulunmaktadır. Örneğin, Yığıt, Kuyruk ve Çift Başlı Kuyruk bu bölümde açıkladığımız Dizi Yığıt, Dizi Kuyruk ve Çifte Dizi İki Başlı Kuyruk yapılarına kolayca genellenebilir. Knuth bu yapıların gerçekleştirimlerini [46, Bölüm 2.2.2]’de tartışmıştır. Kök Dizi Yığıt’ı 2.6.2’deki gibi ilk olarak Brodnik ve arkadaşlarının [13] tanımlamış oldukları ve Bölüm bir alt-sınırı ispatladıkları görülmektedir. Onlar aynı zamanda i2b (i) yöntemindeki karekök işlemini önlemek amacıyla blok boyutlarının daha karmaşık seçildiği farklı bir yapı ortaya koymuşlardır. Onların tasarımında, i’nin dahil olduğu blok ile endekslenen bloktur. Endeksi ise i+1’in ikili tabanda ifadesinin en başında yer alan 1 bitinin lokasyonudur. Bazı bilgisayar mimarileri bir tamsayının başında gelen 1-bitinin lokasyonunu hesaplayan komutu otomatik olarak sağlar. Kök Dizi Yığıt ile ilgili bir yapı da Goodrich and Kloss’un iki seviyeli katmanlı vektörüdür [35]. Bu yapı al (i, x) ve belirle(i, x) işlemlerini sabit sürede ve ekle (i, x) ve sil (i) işlemlerini zamanda destekler. Alıştırma 2.11’de tartışılan Kök Dizi Yığıt’ın daha dikkatli bir gerçekleştiriminden elde edilen çalışma zamanlarının benzeridir. Alıştırma 2.1. Dizi Yığıt uygulamasında sil (i) metoduna yapılan ilk çağrı sonrasında a arkalık dizisi Dizi Yığıt sadece n eleman saklamasına rağmen n + 1 boş olmayan değeri içerir. Ekstra 67 boş olmayan değer nerededir? Bu boş olmayan değerin, Java Çalıştırma Ortamı bellek yöneticisinde olması hangi sonuçları getirir? Tartışın. Alıştırma 2.2. Liste veri yapısının yöntemlerinden hepsiniEkle (i, c) her bir toplanmış veri c elemanını listenin i pozisyonuna ekler. (ekle (i, x) yöntemi c = {x} olan bir özel durumdur.) Bu bölümdeki veri yapıları için hepsiniEkle (i, c) yöntemini yinelenen ekle (i, x) çağrıları ile gerçekleştirmenin niçin verimli olmadığını açıklayın. Daha verimli bir uygulamayı tasarlayın ve uygulayın. Alıştırma 2.3. Bir Rasgele Kuyruk tasarlayın ve uygulayın. Bu, sil () işleminin şu anda kuyrukta yeralan tüm elemanlar arasında rasgele eşitlikte seçilmiş bir öğeyi kaldırdığı Kuyruk arayüzünün bir uygulamasıdır. (Rasgele Kuyruk’u öğeleri eklemek veya ulaşmak ve körü körüne bazı rasgele elemanları çıkarmak için herhangi bir torba (bag) gibi bir düşünün.) Bir Rasgele Kuyruk’un ekle (x) ve sil () işlemleri işlem başına sabit zaman çalışmalıdır. Alıştırma 2.4. Bir Üç Uçlu Kuyruk’u tasarlayın ve uygulayın. Bu al(i) ve belirle(i, x) işlemlerinin sabit zamanda çalıştığı ve ekle (i, x) ve sil (i) işlemlerinin zamanda çalıştığı bir Liste uygulamasıdır. Diğer bir deyişle, her iki uca yakınsa veya liste ortasına yakın ise değişiklikler hızlı olur. Alıştırma 2.5. a dizisinin elemanlarını “dönüştürerek yer değiştirmesini” sağlayan elemanYerDönüştür (a, r) yöntemini her i ∈ {0, …, a.length} için a [i] pozisyonundaki elemanın gideceği yer a [(i + r) mod a.length] olacak şekilde uygulayın. Alıştırma 2.6. Liste elemanlarının yerlerini dönüştürerek değiştirecek bir elemanYerDönüştür (r) yöntemini i pozisyonundaki bir liste elemanının yeri (i + r) mod n pozisyonunda olacak şekilde gerçekleştirin. Dizi İki Başlı Kuyruk veya Çifte Dizi İki Başlı Kuyruk üzerinde çalıştırdığınızda elemanYerDönüştür (r) yönteminin çalışma zamanı O(1 + min {r, n – r}) olmalıdır. 68 Alıştırma 2.7. Dizi İki Başlı Kuyruk gerçekleştirimini ekle (i, x), sil (i), ve yeniden_boyutlandır () tarafından Sistem.arraycopy(s, i, d, j, n) yapılan kaydırmayı daha hızlı yapmak için yöntemini kullanarak değiştiriniz. Alıştırma 2.8. Dizi İki Başlı Kuyruk gerçekleştirimini % operatörünü kullanmayacak şekilde uygulayın (nedeni bu bazı sistemlerde pahalıdır). Bunun yerine, a.length 2 üssü bir sayıysa eşitliğinden yararlanmak gerekir. (Burada, & bit-bit-ve operatörüdür.) Alıştırma 2.9. Hiç bir modüler aritmetik yapmayan Dizi İki Başlı Kuyruk gerçekleştirimini tasarlayın ve uygulayın. Bunun yerine, tüm veriler, bir dizi içinde, sırayla, ardışık blok içinde oturur. Veri bu dizinin başlangıcına ya da sonuna taştığı zaman, yeniden_inşa() işlemi gerçekleştirilir. Tüm işlemlerin amortize edilmiş maliyeti Dizi İki Başlı Kuyruk ile aynı olmalıdır. Alıştırma 2.10. İsraf alanı sadece olan, ancak ekle (i, x) ve sil (i, x) işlemlerini O(1 + min {i, n - i} ) zamanda gerçekleştirebilen bir Kök Dizi Yığıt versiyonunu tasarlayın ve uygulayın. Alıştırma 2.11. İsraf alanı sadece olan, ancak ekle (i, x) ve sil (i, x) işlemlerini zamanda gerçekleştirebilen bir Kök Dizi Yığıt versiyonunu tasarlayın ve uygulayın (Fikir edinmek için, bakınız Bölüm 3.3.) Alıştırma 2.12. İsraf alanı sadece olan, ancak ekle (i, x) ve sil (i, x) işlemlerini zamanda gerçekleştirebilen bir Kök Dizi Yığıt versiyonunu tasarlayın ve uygulayın (Fikir edinmek için, bakınız Bölüm 3.3.) Alıştırma 2.13. Bir Kübik Dizi Yığıt tasarlayın ve uygulayın. Bu üç seviyeli yapıda Liste arayüzünün kullandığı israf alanı O(n2/3) iken ekle (i, x) ve sil (i) işlemlerinin amortize çalışma zamanı O(n1/3) olur. al (i) ve belirle (i, x) işlemleri sabit zamanda çalışır. 69 Bölüm 3 Bağlantılı Listeler Bu bölümde, Liste arayüzünün uygulamalarını çalışmaya devam edeceğiz, bu kez dizi tabanlı veri yapılar yerine işaretçi-tabanlı veri yapılarını kullanacağız. Bu bölümde inceleyeceğimiz yapılar liste elemanlarını içeren düğümlerden oluşmaktadır. Düğümler referanslar (işaretçiler) yoluyla bir dizi halinde bir araya bağlanır. Biz ilk olarak tek başına-bağlı listeleri çalışacağız. Bu yapı Yığıt ve (FIFO) Kuyruk işlemlerini işlem başına sabit zamanda çalıştırır. Daha sonra çifte-bağlantılı listelere geçeceğiz. Bu yapı İki Başlı Kuyruk işlemlerini sabit zamanda çalıştırır. Liste arayüzünün dizi tabanlı uygulamalarını bağlantılı listeler ile kıyasladığımızda bazı avantaj ve dezavantajlar ile karşılaşırız. Birinci dezavantajı herhangi bir elemana erişmek için sabit zamanlı al (i) ve belirle (i, x) işlemlerini kullanma yeteneğimiz kaybedilebilir. Bunun yerine, i'nci elemana ulaşana kadar liste üzerindeki her bir eleman üzerinden yürümek zorunda kalırız. En önemli avantajı ise, daha dinamik olmasıdır: u herhangi bir liste düğümü olduğu takdirde u listenin hangi pozisyonunda olursa olsun, u için bir referans verilirse, u silinebilir veya u’ya bitişik bir düğüm sabit zaman içinde eklenebilir. 3.1 TBListe: Tekli-Bağlantılı Liste Bir TBListe (tekli-bağlantılı liste) bir dizi Düğüm’den oluşur. Her u düğümü bir u.x verisi ve dizideki bir sonraki düğüme u.next işaretçisi saklar. Dizinin son düğümü w için, w.next = null olur. 70 Verimliliği artırmaya yarayan head ve tail değişkenleri birinci ve son düğümü izlemek için kullanılır. Bunun yanı sıra dizinin uzunluğunu takip etmek için bir n tamsayısı kullanılır: TBListe üzerinde gerçekleştirilebilen Yığıt ve Kuyruk işlemlerinden bazıları Şekil 3.1 'de gösterilmiştir. Dizinin başındaki elemanları ekleyerek ve çıkararak yığ () ve yığıttan kaldır () Yığıt işlemleri TBListe tarafından verimli şekilde gerçekleştirilir. yığ () işlemi basitçe veri değeri x olan yeni bir düğüm u oluşturur, bunu u.next değişkenini kullanarak eski listeye ekler ve u listenin yeni başı olur. En sonunda n bir artırılır çünkü TBListe’in boyutu bir artmıştır: 71 yığıttan kaldır () işlemi, TBListe düğümlerinin boş olmadığı kontrol edildikten sonra listenin başını head=head.next olarak belirler ve n bir azaltılır. Son eleman kaldırılırken özel bir durum oluşur ki burada, listenin sonu tail=null olarak belirlenmiştir. Açıktır ki, yığ (x) ve yığıttan kaldır () işlemlerinin her ikisi de O (1) zamanda çalışıyor. 3.1.1 Kuyruk İşlemleri Bir TBListe ekle (x) ve sil () FIFO Kuyruk işlemlerini sabit zamanda gerçekleştirir. Çıkarma listenin başından yapılır, ve yığıttan kaldır () işlemi ile eşdeğerdir: 72 Diğer yandan, eklemeler listenin sonundan yapılır. Çoğu durumda, bu tail.next = u belirlenmesi ile gerçekleştirilir, burada u, x verisini içeren yeni oluşturulan düğümdür. Ancak n=0 olduğunda, özel bir durum ortaya çıkar. Bu özel durumda tail = head = null olarak belirlenmiştir. Ekleme sonucunda, hem tail ve hem de head, u olarak ayarlanır. Açıktır ki, ekle (x) ve sil () işlemlerinin her ikisi de O (1) zamanda çalışıyor. 3.1.2 Özet Aşağıdaki teorem TBListe performansını özetler: Teorem 3.1. Bir TBListe, Yığıt ve (FIFO) Kuyruk arayüzlerini uygular. yığ(x), yığıttan kaldır(), ekle(x) ve sil() işlemleri işlem başına O(1) zamanda çalışır. Bir TBListe neredeyse İki Başlı Kuyruk işlemlerinin tam setini uygular. Tek eksik işlem TBListe düğümlerinin sonuncusunu ortadan kaldırmaktır. Son TBListe düğümünün silinmesi zordur, çünkü öyle bir şekilde güncellenmesi gerekir ki bu, TBListe içindeki sondan bir önceki w düğümüne işaret etsin, bu w.next = tail olan w düğümüdür. Ne yazık ki, w’ya ulaşmak için tek yol TBListe’in en başındaki düğümden başlayarak adım adım takip edilmesi ve en sonunda n-2 adım geriye gitmektir. 73 3.2 ÇBListe: Çifte-Bağlantılı Liste Bir ÇBListe (çifte-bağlantılı liste) TBListe’ye çok benzer, ancak ÇBListe’deki her u düğümünün onun ardından gelen u.next ve öncesinden gelen u.prev bağlantıları mevcuttur. Bir TBListe uygulamasında, endişelenecek sadece birkaç özel durum olduğunu gördük. Örneğin, son elemanı silmek, ya da boş bir TBListe’ye eleman eklenmesi listenin baş ve son elemanlarının doğru olarak güncellenmesini gerektirir. ÇBListe’ye baktığımızda özel durumların sayısı önemli ölçüde artmıştır. Belki de bir ÇBListe için tüm bu özel durumların üstesinden gelmenin en temiz yolu bir dummy düğüm tanıtmaktır. Bu herhangi bir veri içermeyen bir düğüm olarak tanımlanır, sadece bir yer tutucu görevi görür. Bu sayede özel düğüm kavramı ortadan kalkmış olur; her düğümün bir sonraki ve bir önceki bağlantısı mevcut olur. Listedeki son düğümü izleyen düğüm dummy düğüm olarak adlandırdığımız düğümdür. Listedeki ilk düğümden önce gelen düğüm yine dummy düğüm olarak adlandırdığımız düğümdür. Böylece, Şekil 3.2’de gösterildiği gibi listenin düğümleri, bir döngü içine iki kat olmak üzere çifte bağlanmıştır. Belirli bir endeks pozisyonundaki ÇBListe elemanını bulmak zor değildir, biz ya listenin başından başlayabiliriz (dummy.next) ve ileri yönde çalışırız, ya da listenin sonundan başlar (dummy.prev) ve geri yönde çalışırız. Bu bize i'nci düğüme erişmek için O(1+min {i, n – i}) zaman gerektirir: 74 al (i) ve belirle (i, x) işlemleri de artık kolaydır. Gerçekleştirmek için ilk olarak i'nci düğümü buluruz ve daha sonra x değerini alır ya da belirleriz. Bu işlemlerin çalışma zamanı i’nci düğümü bulmak için gerekli zaman ile sınırlıdır, ve bu nedenle O(1+min {i, n – i}) olur. 3.2.1 Ekleme ve Çıkarma Bir ÇBListe’de yer alan w düğümüne başvuru yapıldıysa, ve w düğümünden önce bir u düğümü eklemek istiyorsak, o zaman basitçe u.next=w ve u.prev = w.prev olarak belirlendikten sonra gerekli u.prev.next ve u.next.prev ayarlamaları yapılmalıdır (bkz. Şekil 3.3). Dummy düğüm sayesinde, w.prev ve w.next’in mevcut olmaması durumları ihtimal dahilinde değildir. 75 ekle (i, x) liste işlemini gerçekleştirmek şimdi çok basittir. ÇBListe’de yer alan i’nci düğümü buluruz. x verisini taşıyan yeni bir u düğümünü hemen öncesinden ekleriz. ekle(i, x) işleminin sabit olmayan tek çalışma süresi bileşeni bu i'nci düğümü bulmak için gereken zamandır ( alDüğüm (i) kullanarak). Bu nedenle, ekle (i, x) O(1+min {i, n – i}) zamanda çalışır. ÇBListe’den bir w düğümünü silmek kolaydır. Sadece w.prev ve w.next işaretçilerini ayarlamamız gereklidir; öyle ki listenin geri kalanı w düğümünü atlasın. Yine, dummy düğümün kullanımıyla herhangi bir özel durumu dikkate alma ihtiyacı ortadan kalkar: Şimdi sil (i) işlemi çok kolaydır. i endeksli düğümü bulup listeden çıkarırız: 76 Yine, bu işlemin sadece en pahalı bileşeni alDüğüm (i) kullanarak i’nci düğümü bulmaktır. Bu nedenle, sil (i) O(1+min {i, n – i}) zamanda çalışır. 3.2.2 Özet Aşağıdaki teorem ÇBListe performansını özetler: Teorem 3.2. Bir ÇBListe, Liste arayüzünü uygular. Bu gerçekleştirimde al(i), belirle(i, x), ekle(i, x) ve sil(i) işlemleri işlem başına O(1+min {i, n – i}) zamanda çalışır. Değinilmesi gereken bir konu da, alDüğüm (i) işlem maliyeti görmezden gelinirse, o zaman tüm ÇBListe işlemleri sabit zaman alır. Bu durumda, bir ÇBListe üzerindeki işlemlerin tek pahalı bileşeni ilgili düğümü bulmaktır. İlgili düğüme eriştikten sonra, düğüm ekleme, silme, ya da o düğüme ait veri erişimi sadece sabit zaman alır. Bu Bölüm 2’deki dizi tabanlı Liste uygulamalarının zıttıdır. Bu gerçekleştirimlerde, ilgili dizi elemanı sabit bir süre içinde bulunabilmektedir. Bununla birlikte, ekleme veya silme dizide elemanların yerlerinin değişmesini gerektirdiği için genel olarak, sabit olmayan bir zaman almaktadır. Bağlantılı liste yapıları liste düğümlerine referansların dış yollarla elde edilebildiği uygulamalar için çok uygundur. Bunun bir örneği, Java Koleksiyonlar Çerçevesi’nde bulunan LinkedHashSet veri yapısıdır. Bu yapıda bir dizi eleman çifte bağlı listede saklanır ve çifte- bağlantılı liste düğümleri (Bölüm 5’te tartışılan) karma bir tabloda saklanır. Bir LinkedHashSet elemanını silmek istediğinizde, karma tablo sabit zamanda ilgili liste düğümünü bulmak için kullanılır ve daha sonra listedeki düğüm sabit bir süre içinde silinir. 77 3.3 AVListe: Alan-Verimli Liste Bağlantılı listelerinin sorunlardan biri (listenin derinlerinde olan elemanlara erişmek için gereken zaman yanında) kullanım alanı ile ilgilidir. Bir AVListe’nin her düğümü kendisinden sonraki ve önceki düğümlere ait iki ek başvuru gerektirir. Bir düğümün iki bilgi alanı listenin bakımı için ve sadece bir alan veri depolamak için ayrılmıştır! Bir AVListe (alan-verimli liste), basit bir fikir kullanarak harcanan bu alanı azaltır: Liste elemanlarını bir ÇBListe’de saklamak yerine, çeşitli elemanları içeren (dizi) blokta saklarız. Daha doğrusu, bir AVListe bir blok boyutu b tarafından parametrelendirilmiştir. Bir AVListe’de yer alan her tek düğüm b + 1 elemana kadar tutabilecek alana sahip bir blok saklar. Daha sonra açık hale gelecek nedenlerden dolayı, eğer her blokta İki Başlı Kuyruk işlemlerini yapabilirsek yararlı olacaktır. Bunun için seçtiğimiz veri yapısı Bölüm 2.4 'de anlatılan Dizi İki Başlı Kuyruk Kuyruk küçük yapısından türetilmiş olan Sınırlı İki Başlı Kuyruk ’tur. Sınırlı İki Başlı bir şekilde Dizi İki Başlı Kuyruk’tan farklıdır: Yeni bir Sınırlı İki Başlı Kuyruk oluşturulduğunda, destek dizi olan a dizisinin büyüklüğü b + 1 ile sabitlenmiştir, boyutu hiçbir zaman artmaz ya da azalmaz. Bir Sınırlı İki Başlı Kuyruk’un önemli bir özelliği elemanları başa veya sona sabit zamanda eklemeye veya silmeye olanak tanımasıdır. Elemanların bir bloktan bir başka bloğa kaydırılması esnasında bu özellikten yararlanacağız. Bunun anlamı, bir AVListe çifte-bağlantılı liste bloklarından oluşmaktadır: 78 3.3.1 Alan Gereksinimleri Bir bloğun eleman sayısına bir AVListe çok sıkı kısıtlamalar getirmektedir: Bir blok son blok olmadığı sürece, o blok en az b – 1 ve en çok b + 1 eleman içerir. Bunun anlamı bir AVListe n eleman içeriyorsa, o zaman blok sayısı en fazla olur. Her blok için Sınırlı İki Başlı Kuyruk b + 1 uzunluğunda bir dizi içerir, ancak, sonuncu blok dışındaki her blok için, en fazla sabit miktarda bir alan bu dizide israf edilmektedir. Bir blok tarafından kullanılan bellek de sabittir. Bunun anlamı, bir AVListe’de israf edilen alan sadece O(b + n/b) ’dir. Sabit bir faktörü içinde bir b değeri seçerek biz AVListe’nin alan- gereksinimini Bölüm 2.6.2 'de verilen 3.3.2 alt sınırına yaklaştırabiliriz. Elemanların Bulunması Biz AVListe ile yüzleştiğimizde ilk motivasyonumuz, belirli bir i endeksine ait liste elemanını bulmaktır. Bir elemanın listedeki yerini oluşturan iki bölüm şunlardır: 1. i endeksli elemanı içeren bloğu içeren u düğümü; ve 2. kendi bloğu içinde elemanın j endeksi. 79 Belirli bir öğeyi içeren bir bloğu bulmak için, ÇBListe’de olduğu gibi aynı şekilde devam etmeliyiz. Ya listenin başından başlayıp ileri yönde hareket edeceğiz, veya listenin arkasından başlayıp istenen düğüme ulaşana kadar geriye doğru hareket edeceğiz. Bir düğümden ötekine geçerken bütün bloğu bir seferde atlarız. Unutmayın ki, en fazla bir blok dışında, her blok en az b - 1 eleman içerir, bu nedenle aramamız sırasında atılan her adım bizi aradığımız elemana b - 1 eleman kadar yaklaştırır. İleriye doğru arama yapıyorsanız, O(1 + i / b) adım sonrasında istediğimiz düğüme ulaşılması anlamına gelir. Geriye doğru arama yapıyorsanız, O(1 + (n – i) / b) adım sonrasında istediğimiz düğüme ulaşılması anlamına gelir. Algoritma, i değerine bağlı olarak 80 bu iki miktarın daha küçüğünü alır, böylece i endeksli elemanı bulmak için gereken zaman O(1 + min { i, n – i } / b ) olur. i endeksli elemanı nasıl bulacağımızı biliyorsak, al (i) ve belirle (i, x) işlemleri doğru blokta belirli bir endeksi almayı veya belirlemeyi gerektirir: Bu işlemlerin çalışma zamanları, elemanı bulmak için gereken zaman ile kısıtlanmıştır, bu nedenle O(1 + min { i, n – i } / b ) zamanda çalışır. 3.3.3 Elemanların Eklenmesi Bir AVListe’ye eleman eklenmesi biraz daha karmaşıktır. Genel durumu dikkate almadan önce, daha kolay olan x elemanının listenin sonuna eklendiği ekle (x) işlemini gözden geçirelim. Son blok doluysa, (veya hiç bir blok henüz mevcut değilse) o zaman önce yeni bir blok tahsis ederiz, ve blok listesine ekleriz. Şimdi son bloğun var olduğundan ve onun boş olmadığından emin halde x elemanını bu bloğa ekleriz. 81 ekle (i, x) kullanarak listenin içinde bir yere eleman eklediğimizde işler biraz daha karışık hale gelir. i'nci liste elemanını içeren u düğümünün bloğuna erişmek için ilk olarak i bulunur. u bloğunun içine x elemanını eklemek sorununu çözmek için u bloğunun zaten b + 1 eleman içerdiği dolu durumu için hazırlıklı olmak zorundayız. Bunun anlamı x için o blokta yer olmadığıdır. u0, u1, u2, ... düğümlerinin işaretçilerini u, u.next, u.next.next olarak belirleyelim. u0, u1, u2, … düğümlerinin x elemanını eklemek için bir alan sağlamasını araştırırken üç durum oluşabilir (bkz. Şekil 3.4): 1. Dolu olmayan bir ur düğümünü hızlıca ( r + 1 ≤ b adımda) buluruz. ur düğümüne ait bloktan diğerine r eleman yer değiştirir, böylece x elemanını eklemek için ur düğümünde serbest alan oluşmuş oluyor. 2. Hızlıca ( r + 1 ≤ b adımda) bulduğumuz blok listelerinin sonuna geliriz. Bloğun sonuna yeni bir boş blok ekleriz ve birinci halde olduğu gibi devam ederiz. 3. Dolu olmayan bir ur düğümünü bulamıyoruz. Bu durumda, u0, …, ub-1 b adet bir dizi bloğudur. Bu blokların her biri b + 1 eleman içerir. En son bloğun ardına yeni bir blok ub bloğu ekleriz ve orijinal b (b + 1) elemanları yayarız. Böylece her bir u0, …, ub 82 bloğu tam olarak b eleman içerir. u0 bloğu şimdi tam olarak b eleman içermektedir ve bu nedenle eleman eklenebilir. ekle (i, x) işleminin çalışma zamanı yukarıda ortaya konan üç durumdan hangisine bağlı olduğuna göre değişir. 1. ve 2. durumlarda elemanları incelemek ve değiştirmek için en çok b blok kullanılır, bu nedenle O(b) zamanda çalışır. 3. durumda yayıl(u) yöntemi çağrıldığında b (b + 1) eleman yer değiştirir ve O(b2) zamanda çalışır. 3.durumun maliyeti görmezden gelinerek (daha sonra amortizasyon hesabıyla ele alınacaktır) i endeksinin yerini belirlemek ve x elemanını eklemek için gereken toplam zaman O(b + min { i, n – i } / b ) olur. 83 3.3.4 Elemanların Silinmesi Bir AVListe’den eleman silinmesi eklemeye benzerdir. i endeksini içeren u düğümünün yerini bulduktan sonra, u düğümüne ait bloğun b – 1 elemandan daha azına sahip olduğu için bir elemanı u düğümünden silmenin olanaksız olduğu durum için hazırlıklı olunmalıdır. Yine, u0, u1, u2, ... düğümlerinin işaretçilerini u, u.next, u.next.next olarak belirleyelim. u0 bloğunun boyutunun en azından b – 1 olması için u0, u1, u2, … düğümlerinden herhangi biri bize bir eleman ödünç vermelidir. Bunu araştırırken üç durum oluşabilir (bkz. Şekil 3.5): 1. Hızlıca ( r + 1 ≤ b adımda) bloğu b – 1 elemandan daha fazla eleman içeren bir düğüm buluruz. Bu durumda, bir bloktan öncekine r eleman yer değiştirir. Böylece ur düğümünün ekstra blok elemanı u0 düğümünün ekstra blok elemanı haline gelir. Bunu takiben u0 bloğundan istenen eleman silinir. 2. Hızlıca ( r + 1 ≤ b adımda) bulduğumuz blok listelerinin sonuna geliriz. Bu durumda, ur düğümüne ait blok son bloktur ve ur düğümüne ait bloğunun en az n – 1 eleman içermesine gerek yoktur. Bu nedenle, biz, yukarıdaki gibi devam etmeliyiz ve u0 düğümünün ekstra elemanı yapmak için ur düğümünden bir elemanı ödünç almalıyız. Bu ödünç alma sonrasında ur düğümünün bloğu boş hale gelirse, listeden silinir. 84 3. b adım sonrasında en fazla b-1 eleman içeren herhangi bir blok bulamadığımız takdirde u0, ..., ub-1 her biri b-1 eleman içeren b adet dizi bloğu olmuştur. Tüm bu b(b-1) elemanı b-1 adet bloğun her birinde tam olarak b eleman olacak şekilde, u0, ..., ub-2 düğüm halinde yeniden yapılandırırız ve şimdi boş olan, ub-1 düğümünü sileriz. u0 bloğu şimdi b eleman içermektedir ve bu nedenle eleman silinebilir. ekle(i, x) işlemine benzer şekilde sil(i) işleminin çalışma zamanı 3.durumdaki birarayagetir (u) yönteminin maliyeti görmezden gelinirse O(b + min { i, n – i } / b ) olur. 85 3.3.5 yayıl (u) ve birarayaGetir (u) Yöntemlerinin Amortize Edilmiş Analizi Şimdi, ekle (i, x) ve sil (i) işlemleri tarafından çağrılabilen yayıl (u) ve birarayaGetir (u) yöntemlerinin maliyetini düşünüyoruz. Bütünlüğü sağlamak adına bunlar aşağıda verilmiştir: Bu yöntemlerin her birinin çalışma süresi iki iç içe döngü tarafından kısıtlanmıştır. Hem iç ve hem de dış döngüler, en fazla b + 1 defa çalışır, bu nedenle, bu yöntemlerin her birinin toplam çalışma süresi O((b+1)2) = O(b2) olur. Ancak, aşağıdaki önerme bu yöntemlerin ekle(i, x) ve sil (i) yöntemlerine yapılan her b’nci çağrıda bir defa çalıştırıldığını gösterir. 86 Önerme 3.1. Boş bir AVListe oluşturulursa, ve ekle(i, x) ve sil (i) yöntemlerine m ≥ 1 defa çağrı yapılırsa, o zaman yayıl () ve birarayaGetir () yöntemlerine yapılan tüm çağrılar sırasında harcanan toplam zaman O(bm) olur. Kanıt. Burada yine bize güç vaat eden amortize analiz yöntemini kullanacağız. Bir u düğümüne ait blok b eleman içermiyorsa düğümün kırılgan olduğu söylenir (öyle ki u ya son düğümdür, ya da bloğunda b – 1 ya da b + 1 eleman vardır.) Bloğunda b eleman içeren herhangi bir düğümün engebeli olduğu söylenir. Burada biz sadece ekle (i, x) işlemini ve yayıl (u) yöntemine yaptığı çağrı sayısı ile olan ilişkisini dikkate alacağız. sil (i) ve birarayaGetir (u) yöntemlerinin analizi benzerdir. Bir AVListe’nin potansiyeli içerdiği kırılgan düğümlerin sayısı olarak tanımlanır. ekle(i, x) çalıştığında 1.durum oluştuğunda, sadece bir ur düğümünün boyutu değişmiştir. Bu nedenle, en fazla bir ur düğümü engebeli olmaktan çıkar, kırılgan olmaya geçer. 2.durum oluştuğunda, yeni bir düğüm oluşturulur ve bu düğüm kırılgandır ancak başka hiçbir düğüm boyut değiştirmez. Bu nedenle kırılgan düğümlerin sayısı bir artar. Böylece, 1. veya 2. durumlarda AVListe’nin potansiyeli en fazla bir artar. Son olarak, 3.durum oluştuğunda, bunun nedeni u0, ..., ub-1 düğümlerinin hepsinin kırılgan olmasıdır. Bunu takiben yayıl () çağrılır ve bu b kırılgan düğüm b + 1 engebeli düğüm ile yer değişir. Son olarak, x elemanı u0 bloğuna eklenir, ve u0 kırılgan hale gelir. Toplam olarak potansiyel b-1 kadar düşer. Özet olarak, potansiyelin başlangıç değeri 0 olur (listede hiçbir düğüm yoktur). 1.durum veya 2.durum her oluştuğunda potansiyel en çok 1 artar. 3.durum oluştuğunda potansiyel b - 1 azalır. (Kırılgan düğüm sayısı olan) potansiyel hiçbir zaman 0'dan az değildir. Şu sonuca varabiliriz ki, 3.durum her oluştuğunda 1.durum veya 2.durum en az b - 1 defa oluşmuştur. Böylece, yayıl (u) yöntemine yapılan her çağrı için ekle (i, x) yöntemine en azından b çağrı yapılmıştır. Bu kanıtı tamamlar. 87 3.3.6 Özet Aşağıdaki teorem AVListe veri yapısının performansını özetler: Teorem 3.3. AVListe Liste arayüzünü uygular. yayıl (u) ve birarayaGetir (u) çağrılarının maliyetini dikkate almayarak, blok boyutu b olan bir AVListe • işlem başına O(1 + min { i, n – i } / b ) zamanda çalışan al (i) ve belirle (i, x); ve • işlem başına O(b + min { i, n – i } / b ) zamanda çalışan ekle (i, x) ve sil (i) işlemlerini destekler. Bundan başka, boş bir AVListe ile başlandığında, ekle (i, x) ve sil (i) işlemleri toplam m defa çağrılırsa, tüm yayıl (u) ve birarayaGetir (u) çağrıları için gerekli toplam çalışma süresi O(bm) olur. n elemanlı bir AVListe’nin kullandığı alan miktarı6 n + O(b + n / b) olarak ifade edilir. AVListe’nin gerçekleştirimi Dizi Liste ve ÇBListe arasında seçim yapmayı gerektiriyor. Blok büyüklüğü b göz önünde tutularak bu iki yapıdan biri göreceli olarak seçilmelidir. En uç noktada b=2 iken, her AVListe düğümü en çok üç değer depolar. ÇBListe de hemen hemen aynıdır. Diğer bir uçta b > n iken, Dizi Liste’de olduğu gibi tüm elemanlar sadece bir dizide saklanabilir. Bu iki uç arasında iseniz, bir liste elemanını eklemek veya kaldırmak için gereken zaman ile belirli bir liste elemanını bulmak için gereken zaman arasında bir seçim yapmanız gerekiyor. 3.4 Tartışma ve Alıştırmalar Tekli-bağlantılı ve çoklu-bağlantılı listelerin her ikisi de 40 yıldan bu yana programlarda kullanılmış olan ve kabul edilmiş tekniklerdir. Bunlar örneğin Knuth tarafından tartışılmıştır [46, Bölüm 2.2.3–2.2.5]. AVListe veri yapıları için iyi bilinen bir alıştırma gibi görünüyor. Hatta AVListe döngüsüz bağlantılı liste olarak adlandırılabilir [69]. 6 Belleğin nasıl ölçüldüğünü Bölüm 1.4’teki tartışmadan hatırlayın. 88 Bir çift-bağlantılı listede alan tasarrufu yapmanın bir başka yolu XOR-listelerini kullanmaktır. XOR listesinde, her u düğümü, u.önceki ve u.sonraki’ni bit-bit x-veya işlemine tabi tutan bir u.sonrakiönceki adında işaretçi ile gösterilir. Listenin kendisi dummy, ve (ilk düğüme, ya da liste boşsa dummy’e eşit olan) dummy.sonraki olmak üzere iki işaretçi saklar. Bu teknik u ve u.önceki işaretçilerini kullanarak u.sonraki = u.önceki ^u.sonrakiönceki formülüyle u.sonraki değerinin hesaplanabileceğini bize anlatır. (Burada ^ işleci iki argümanının bit-bit x-veya hesabını yapmakta kullanılmıştır.) Bu teknik kodu biraz zorlaştırır, ve – Java dahil – çöp toplayan bazı dillerde bu tekniği uygulamak mümkün değildir, ancak düğüm başına yalnızca bir işaretçi gerektiren bir çifte-bağlantılı liste uygulamasını bize sunmaktadır. XOR-listelerinin ayrıntılı bir tartışması için bkz. Sinha’nın dergi makalesi [70]. Alıştırma 3.1. Bir TBListe’de yığ (x), yığıttan kaldır (), ekle (x) ve sil () işlemlerinde ortaya çıkabilecek tüm özel durumları önlemek için bir dummy düğüm kullanılması neden mümkün olmaz? Alıştırma 3.2. TBListe’nin sondan ikinci elemanını döndüren sondanİkinci () yöntemini liste eleman sayısını saklayan n değişkeni olmadan tasarlayın ve uygulayın. Alıştırma 3.3. al (i), belirle (i, x), ekle (i, x) ve sil (i) Liste yöntemlerini TBListe işlemlerine uygulayın. Bu işlemlerin her biri O(1 + i) zamanda çalışmalıdır. Alıştırma 3.4. TBListe’nin tüm elemanlarını ters yönde sıralayan tersSırala () yöntemini tasarlayın ve uygulayın. Bu yöntem O(n) zamanda çalışmalıdır, özyineleme, herhangi bir ikincil veri yapısını kullanması ve yeni düğümleri oluşturması istenmiyor. Alıştırma 3.5. boyutKontrol() adında birer TBListe ve ÇBListe yöntemini tasarlayın ve uygulayın. Bu yöntemler liste elemanları üzerinden adım adım giderek listede kayıtlı bulunan 89 n değeri ile eşleşip eşleşmediğini düğüm sayısını sayarak bulurlar. Bu yöntemlerin dönüş değerleri yoktur, ancak hesapladıkları boyut n değeri ile eşleşmiyorsa bir istisna fırlatırlar. Alıştırma 3.6. önceyeEkle () yönteminin kodunu bir u düğümü oluşturarak w düğümünden hemen önce ÇBListe’ye ekleyecek şekilde yeniden oluşturmayı deneyin. Bu bölümden alıntı yapmayın. Kodunuz tam olarak bu kitapta verilen kod ile eşleşmiyor olabilir. Bu kodun doğru olmadığı anlamına gelmez. Çalışıp çalışmadığını test ederek görün. Bundan sonraki birkaç alıştırma ÇBListe üzerinde yapılabilecek değişiklikleri içeriyor. Herhangi bir yeni düğüm veya geçici dizi tahsis etmeden bunları tamamlamanız gerekiyor. Sadece mevcut düğümlerin önceki ve sonraki değerlerini değiştirerek bunları yapabilirsiniz. Alıştırma 3.7. Liste bir palindrom olduğunda doğru döndüren palindromu () adında bir ÇBListe yöntemi yazın, yani her i ∈ {0, …, n – 1} için i pozisyonundaki eleman n – i – 1 pozisyonundaki elemana eşittir. Kodunuz O(n) zamanda çalışmalıdır. Alıştırma 3.8. ÇBListe elemanlarının yerlerini dönüştürerek değiştirecek bir elemanYerDönüştür (r) yöntemini i pozisyonundaki bir liste elemanının yeri (i + r) mod n pozisyonunda olacak şekilde gerçekleştirin. Bu yöntemin çalışma zamanı O(1+min {r, n–r}) olmalıdır. Alıştırma 3.9. Bir ÇBListe’nin i pozisyonunda liste içeriğini kısaltan içeriğiKısalt (i) adında bir yöntem yazın. Bu yöntemi çalıştırdıktan sonra listenin boyutu i olacak şekilde belirlenmeli ve sadece 0, …, i - 1 endekslerindeki elemanları içermelidir. Dönüş değeri i, …, n – 1 endekslerindeki elemanları içeren başka bir ÇBListe’dir. Bu yöntemin çalışma zamanı O(min {i, n – i}) olmalıdır. Alıştırma 3.10. l2 argümanında bir ÇBListe alan, bu listeyi boşaltarak içeriğini onu çağıran listeye, sırasıyla, ekleyen em(l2) adında bir ÇBListe yöntemini yazın. Örneğin, l1 elemanları a, b, c ve l2 elemanları d, e, f olduğu takdirde, l1.em (l2) çağrıldıktan sonra l1 içeriği a, b, c, d, e, f ve l2 içeriği boş olur. Alıştırma 3.11. ÇBListe’den tüm tek sayılı endekslere ait olan elemanları ortadan kaldıran ve bu elemanları içeren bir ÇBListe döndüren tekliEndeksElemanları () adında bir ÇBListe 90 yöntemi yazın. Örneğin, l1 elemanları a, b, c, d, e, f olduğu takdirde, l1.tekliEndeksElemanları () çağrıldıktan sonra l1 elemanları a, c, e olmalı ve dönüş değeri olarak elemanları b, d, f olan bir liste dönmelidir. Alıştırma 3.12. ÇBListe’nin elemanlarını ters sırada yeniden yapılandıran bir ters () yöntemi yazın. Alıştırma 3.13. Bu alıştırma Bölüm 11.1.1’de tartışıldığı gibi bir ÇBListe sıralamasını gerçekleştirmek için size birleştirme-sıralama algoritmasının bir uygulamasını tanıtıyor. Uygulamanızda, elemanlar arasında karşılaştırmalar yapmak için karşılaştır (x) yöntemini kullanın, böylece ortaya çıkan uygulama ile Karşılaştırılabilir arayüzünü uygulayan elemanları içeren herhangi ÇBListe’yi sıralayabilirsiniz. 1. ilkiSeç (l2) adında bir ÇBList yöntemi yazın. Bu yöntem, l2 listesinden birinci düğümü alır ve alıcı listesine ekler. Yeni bir düğüm oluşturmak gerektiği dışında bu ekle (boyut (), l2.sil (0)) çağrısının eşdeğeridir. 2. l1 ve l2 sıralı listelerini alan, onları birleştiren, ve sıralı yeni listeyi döndüren birleştir(l1, l2) adında bir ÇBListe statik yöntemi yazın. Bu süreç sonunda l1 ve l2 listelerinin içeriği boş hale gelir. Örneğin l1 elemanları a, c, d ve l2 elemanları b, e, f olduğu takdirde, bu yöntem a, b, c, d, e, f içeren yeni bir liste döndürür. 1. Listede yer alan elemanları birleştirme-sıralama algoritmasını kullanarak sıralayan sırala () yöntemi yazın. Bu özyinelemeli algoritma şu şekilde çalışır: a) Liste içeriği 0 veya 1 eleman ise o zaman yapacak bir şey yoktur. Diğer durumlarda, b) içeriğiKısalt (boyut() / 2) yöntemini kullanarak listeyi yaklaşık olarak eşit uzunlukta olan iki listeye l1, l2 bölün. c) Özyinelemeli olarak l1 listesini sıralayın. d) Özyinelemeli olarak l2 listesini sıralayın, en sonunda e) l1, l2 listelerini tek sıralı liste haline birleştirin. Sonraki birkaç alıştırma daha zordur ve bir Yığıt’ta saklanan minimum değer ile elemanlar Kuyruk’a eklendikçe ve silindikçe ne olduğuna dair net bir anlayış gerektirir. 91 Alıştırma 3.14. yığ (x), yığıttan kaldır (), ekle (x) ve sil () ve bunun yanı sıra şu anda veri yapısında depolanan en düşük değeri veren min () işlemini destekleyen ve kıyaslanabilir elemanları saklayabilen bir MinYığıt veri yapısını tasarlayın ve uygulayın. Tüm işlemler sabit zamanda çalışacaktır. Alıştırma 3.15. ekle (x), sil (), boyut () ve bunun yanı sıra şu anda veri yapısında depolanan en düşük değeri veren min (x) işlemlerini destekleyen ve kıyaslanabilir elemanları saklayabilen bir MinKuyruk veri yapısını tasarlayın ve uygulayın. Tüm işlemler sabit amortize zamanda çalışmalıdır. Alıştırma 3.16. ekleİlk (x), ekleSon (x), silİlk (), silSon (), boyut () ve bunun yanı sıra şu anda veri yapısında depolanan en düşük değeri veren min () işlemlerini destekleyen ve kıyaslanabilir elemanları saklayabilen bir MinİkiBaşlıKuyruk veri yapısını tasarlayın ve uygulayın. Tüm işlemler sabit amortize zamanda çalışmalıdır. Sonraki alıştırmalar okuyucunun alan-verimli AVListe’nin uygulanması ve analizi hakkında anlayışını test etmek için hazırlanmıştır. Alıştırma 3.17. Bir AVListe bir Yığıt gibi kullanılırsa, (böylelikle AVListe’ye tek değişiklik yığ(x) = ekle (boyut(), x) ve yığıttan kaldır() = sil (size() – 1) kullanılarak yapılırsa) bu işlemler b değerinden bağımsız olarak, sabit amortize zamanda çalışır. Alıştırma 3.18. Tüm İki Başlı Kuyruk işlemlerini işlem başına b değerinden bağımsız olarak sabit amortize zamanda destekleyen bir AVListe versiyonunu tasarlayın ve uygulayın. Alıştırma 3.19. Üçüncü bir değişken kullanmadan iki int değişken değerlerinin bit-bit-veya işleci, ^, kullanılarak nasıl değiş-tokuş edileceğini açıklayın. 92 Bölüm 4 Sekme Listeleri Bu bölümde, çeşitli uygulamaları olan güzel bir veri yapısını tartışacağız: sekme listeleri. Sekme listesinin Liste arayüzü uygulaması al(i), belirle(i, x), ekle(i, x), ve sil(i) işlemlerini O(log n) zamanda gerçekleştirir. Ayrıca Sıralı Küme gerçekleştirimi tüm işlemleri O(log n) beklenen zamanda çalıştırır. Sekme listelerinin etkinliği dayandığı rasgelelikten kaynaklanır. Yeni bir elemanı sekme listesine eklerken, elemanın yüksekliğini belirlemek için rasgele yazı-tura atılır. Sekme listelerinin performansı beklenen çalışma süreleri ve yol uzunlukları cinsinden ifade edilir. Beklenti, sekme listesi tarafından kullanılan rasgele yazı-tura atma üzerinden üstlenilir. Rasgele yazı-tura atma yarı-rasgele sayı (ya da bit) üreteci kullanılarak gerçekleştirilir. 4.1 Temel Yapı Kavramsal olarak, sekme listesi L0, …, Lh tekli-bağlantılı listelerinden oluşan bir dizidir. Her Lr listesi Lr-1 listesinin içindeki elemanların bir alt kümesini içerir. n eleman içeren giriş listesi L0 ile başlarız. Bunu takiben L1 listesini L0 elemanlarından, L2 listesini L1 elemanlarından, ve buna benzer şekilde diğer listeleri oluştururuz. Lr listesinin elemanlarını oluştururken Lr-1 listesinde bulunan her x elemanı için yazı-tura atılır. Yazı gelirse x dahil edilir. Boş bir Lr listesi ile karşılaşıldığında süreç sonlanır. Örnek bir sekme listesi Şekil 4.1 'de gösterilmiştir. 93 Sekme listesinin içinde yer alan bir x elemanının yüksekliği bu elemanın bulunduğu bir Lr listesinin en büyük r değeri ile ölçülür. Buna göre, örneğin, sadece L0’da görünen elemanlar 0 yüksekliğe sahiptir. Birkaç dakikamızı bu konuda düşünmeye ayırırsak, x'in yüksekliği aşağıdaki deney ile belirlenir: Tura gelene kadar art arda yazı-tura atın. Kaç kere yazı geldi? Cevap, şaşırtıcı olmayacağı üzere, bir düğümün beklenen yüksekliği 1'dir. (Tura gelmeden önce iki kez yazı-tura atmayı bekliyoruz, ancak son atışı saymıyoruz.) Bir sekme listesinin yüksekliği en yüksek düğümünün yüksekliğidir. Her listenin başında, bu liste için bir dummy düğüm gibi davranan ve başlangıç olarak adlandırılan özel bir düğüm vardır. Sekme listelerinin anahtar özelliği Lh listesinin başlangıcından L0 listesindeki tüm düğümlere arama yolu olarak adlandırılan kısa bir yol olmasıdır. Bir u düğümü için bir arama yolunun nasıl düzenleneceğini hatırlamak kolaydır (bkz. Şekil 4.2) : Sekme listesinin sol üst köşesinden (Lh listesinin başlangıcı) başlayın ve u düğümünü aşmadığınız sürece her zaman sağa doğru gidin. Aşağıdaki listeye inip getirmeniz için her seferinde aşağı yönde bir adım atmanız gerekir. Daha kesin bir ifadeyle, L0 listesi içindeki u düğümüne bir arama yolunu oluşturmak için Lh listesinin w başlangıcından yola çıkarız. Sonra w.next incelenir. Eğer w.next içinde L0 içindeki u düğümünden önce görünen bir eleman varsa o zaman w = w.next olarak belirlenir. Aksi takdirde, bir alt listeye geçilir ve Lh-1 listesinin w yerinden aramaya devam ederiz. L0 içindeki u düğümünün bir öncesine ulaşıncaya kadar bu şekilde devam ediyoruz. Bölüm 4.4’te ispat edeceğimiz aşağıdaki sonuç arama yolunun oldukça kısa olduğunu göstermektedir: Önerme 4.1. L0 içindeki herhangi bir u düğümü için arama yolunun beklenen uzunluğu en fazla 2 log n + O(1) = O(log n) olur. Bir sekme listesini gerçekleştirmenin alan-verimli bir yolu veri değeri, x, ve bir işaretçi dizisi olan sonraki’ne sahip u düğümlerini tanımlamaktır. u.sonraki[i] L1 listesi içinde u düğümünden sonra gelene işaret etmelidir. Böylece, bir düğümdeki x verisi birden fazla listede yer alsa bile x sadece bir başvuruyla depolanır. 94 Sonraki iki bölüm sekme listelerinin iki farklı uygulamasını tartışıyor. Bu uygulamaların her birinde L0 ana yapıyı saklar (bir eleman listesi veya sıralı bir küme eleman). Bu yapılar arasındaki temel fark, bir arama yolu üzerinden nasıl yol alındığıdır; özellikle Lr-1’e inileceği mi, yoksa Lr içinde sağa doğru mu gidileceği konusunda karar vermede ayrılık gösterirler. 4.2 Sıralı Küme Sekme Listesi : Verimli bir Sıralı Küme Sıralı Küme Sekme Listesi, Sekme Listesi yapısını kullanarak Sıralı Küme arayüzünü gerçekleştirir. Bu şekilde kullanıldığında, L0 listesi Sıralı Küme elemanlarını sıralı bir şekilde depolamaya yarar. bul (x) yöntemi y ≥ x olan en küçük y değeri için arama yolunu izleyerek çalışır. 95 y için arama yolunu izlemek kolaydır: Lr içinde bir u düğümüne rastladığımızda sağdaki u.next[r].x verisine dikkat ederiz. Eğer x > u.next[r].x ise, Lr içinde sağa doğru bir adım atarız; aksi takdirde, Lr-1 yönünde aşağı doğru hareket ederiz. Bu arama sırasında atılan her adım (sağa veya aşağı doğru) sadece sabit zaman alır; böylece, Önerme 4.1 ile, bul (x) yönteminin beklenen çalışma süresi O(log n) olur. Bir eleman eklemeden önce yeni bir düğümün yüksekliği olan, k, Sıralı Küme Sekme Listesi tarafından hesaplanmalıdır. Bu amaçla yazı-tura atmak için bir yöntem bulunmalıdır. Bir rasgele tamsayı, z, seçerek bunu yapabiliriz. Bunun için, ikili moddaki z sayısının sonda gelen 1’leri sayılır:7 Bir Sıralı Küme Sekme Listesi içinde ekle (x) yöntemini gerçekleştirmek için önce x aranır ve yükseklikSeç () yöntemi kullanılarak k belirlenir. Bunu takiben L0,. . . , Lk listeleri x için uç uça birbirine bağlanır. Bunu yapmanın en kolay yolu, arama yolunun Lr listesinden Lr-1 listesine indiği düğümlerin izlerini kaydeden bir Dizi Yığıt kullanmaktır. Daha kesin bir ifadeyle yığıt[r] arama yolunun Lr-1 listesine indiği Lr düğümüdür. x eklenirken değiştirilen düğümler, tam olarak, yığıt[0] … yığıt[k] tarafından kaydedilir. ekle (x) algoritması aşağıda gösterilmiştir: 7 Bu yöntem tam olarak yazı-tura atma deneyinin bir kopyası değildir, çünkü k değeri her zaman int bit sayısından daha az olacaktır. Bununla birlikte, yapı 232=4294967296 sayısından daha fazla eleman içermediği sürece bu önemsiz bir etkiye sahiptir. 96 x elemanını silmek benzer şekilde yapılır, ancak burada arama yolunu takip etmek için yığıt’a gerek duyulmaz. Arama yolu takip edilirken aynı anda silme yapılabilir. x elemanını bulmak için yapılan arama sırasında bir u düğümünden aşağıya doğru yapılan her harekette u.next.x = x eşitliği kontrol edilir; sağlanması durumunda u bir sonraki düğüme listenin dışına doğru uç uça bağlanır. 97 4.2.1. Özet Aşağıdaki teorem Sıralı Küme kullanılarak gerçekleştirilen Sekme Listesi’ nin performansını özetler: Teorem 4.1. Sıralı Küme Sekme Listesi, Sıralı Küme arayüzünü uygular. Sıralı Küme Sekme Listesi işlem başına O(log n) beklenen zamanda çalışan ekle(x), sil(x) ve bul(x) işlemlerini destekler. 98 4.3 Liste Sekme Listesi: Verimli Rasgele Erişim Listesi Liste Sekme Listesi bir Sekme Listesi yapısını kullanarak Liste arayüzünü uygular. Bir Liste Sekme Listesi’nde L0 listesi Sekme Listesi’nde olduğu elemanları listede göründükleri sırayla depolanır. Sıralı Küme gibi, elemanlar O(log n) zamanda eklenir, çıkarılır ve erişilebilir. Bunun mümkün olabilmesi için, i'nci elemana erişmek için gerekli arama yolunu takip etmemiz için bir yol bulmamız gerekir. Bunu yapmanın en kolay yolu herhangi bir Lr listesi için kenar uzunluğu kavramını tanımlamaktır. L0 içindeki her kenarın uzunluğu 1 olarak tanımlanır. r > 0 olacak şekilde Lr listesindeki, e, kenarının uzunluğu Lr-1 içindeki e kenarının altında yer alan uzunlukların toplamı olarak tanımlanmaktadır. Aynı anlama gelecek şekilde, e uzunluğu e altında yer alan L0 kenarlarının sayısıdır. Kenar uzunluklarının gösterildiği bir sekme listesi örneği için Şekil 4.5 'e bakınız. Sekme listelerinin kenarları dizilerde saklandığı için uzunlukları aynı şekilde saklanabilir. 99 Bu uzunluk tanımının kullanışlı bir özelliği, eğer şu anda L0 içinde j pozisyonunda yer alan düğümde bulunuyorsak, ve l uzunluğunda bir kenarı izliyorsak, L0 içinde j + l pozisyonundaki bir düğüme ulaşmamızdır. Bu şekilde, bir arama yolunu takip ederken, L0 içindeyken bulunduğumuz düğümün j pozisyonunu takip edebiliriz. Lr içindeki bir u düğümünde isek, j + u.next[r] kenarının uzunluğu i’den küçükse, sağa doğru gideriz. Aksi takdirde, Lr-1 içine aşağı doğru gideriz. al(i) ve belirle(i, x) işlemlerinin en zor kısmı L0 içindeki i’nci düğümü bulmak olduğu için bu işlemler O(log n) zamanda çalışır. Liste Sekme Listesi Sekme Listesi’nin içindeki i pozisyonuna bir eleman eklemek oldukça kolaydır. Sıralı Küme aksine, burada yeni bir düğümün muhakkak ekleneceğinden eminiz. Ekleme yeni düğümün konumunu ararken aynı zamanda gerçekleştirilebilir. Önce yeni eklenen w düğümünün yüksekliği olan k seçilir ve sonra i için arama yolu izlenir. Ne zaman 100 ki arama yolu r ≤ k olmak üzere Lr ’dan aşağı doğru giderse w için Lr uç uça birleştirilir. Sadece kenar uzunluklarının düzgün olarak güncellendiğinden emin olmak için özen göstermemiz gerekiyor. Bkz. Şekil 4.6. Unutmayın ki, Lr içindeki bir u düğümünden itibaren başlayan arama yolu ne zaman aşağı doğru giderse, u.next [r] kenar uzunluğu bir artırılır, çünkü i pozisyonundaki kenarın altına yeni bir eleman ekliyoruz. w düğümü için listelerin u ve z olarak uç uça birleştirilmesi Şekil 4.7’de gösterildiği gibi çalışır. Arama yolu takip edilirken L0 içindeki u düğümünün j pozisyonu takip edilmektedir. Bu nedenle, u düğümünden w düğümüne kenar uzunluğunun i–j olduğunu biliyoruz. Bunun yanı sıra, aynı zamanda w düğümünden z düğümüne uzanan kenar uzunluğunu, u düğümünden z düğümüne kenar uzunluğu olan l ’ye bakarak çıkarsayabiliriz. Bu nedenle listeler w için uç uça birleştirilir ve kenar uzunlukları sabit zaman içinde güncellenir. 101 Kod oldukça basit olduğu için, bu anlatılanlar size olduğundan daha karmaşık gelmiş olabilir: 102 Artık, Liste Sekme Listesi’nde sil (i) işleminin gerçekleştirimi açık hale geldi. Düğüm için i pozisyonundaki arama yolu izlenir. Arama yolu bir u düğümünden aşağı r seviyesinde her adım attığında, u aynı seviyede kalacak şekilde kenar uzunluğu azaltılır. Aynı zamanda u.next [r] değerinin i’yi gösterdiği kontrol edilir, ve gösteriyorsa listeler o seviyede uç uça birleştirilir. Bir örnek Şekil 4.8 'de gösterilmiştir. 4.3.1 Özet Aşağıdaki teorem Liste Sekme Listesi veri yapısının performansını özetler: Teorem 4.2. Liste Sekme Listesi, Liste arayüzünü uygular. Liste Sekme Listesi işlem başına O(log n) beklenen zamanda çalışan al(i), belirle(i, x), ekle(i, x) ve sil(i) işlemlerini destekler. 103 4.4 Sekme Listelerinin Analizi Bu bölümde, bir Sekme Listesi içindeki arama yolunun beklenen yüksekliği, boyutu ve uzunluğunu analiz edeceğiz. Bu bölüm, temel olasılık konusunda bir miktar bilgi birikiminizin var olmasını gerektirir. Yazı-tura atma ile ilgili aşağıdaki gözlem çeşitli kanıtlamalar için bir temel oluşturmaktadır. Önerme 4.2. T adil bir paranın fırlatıldıktan sonra ilk defa yazı geldiği zamana kadar olan atma sayısı olsun. O zaman E[T] = 2 olur. Kanıt. İlk defa yazı geldiği zaman, yazı-tura atmayı durduğumuzu düşünelim. Gösterge değişkeni tanımlayın : Unutmayın ki, ancak ve ancak ilk i – 1 yazı-tura sonucu yazı geldiği takdirde, Ii = 1 sağlanır, bu nedenle sayısı olur. Gözlemlemelisiniz ki, T, toplam yazı-tura atma ile verilir. Bu nedenle, Sonraki iki önerme, sekme listelerinin doğrusal boyutta olduğunu bize anlatıyor: 104 Önerme 4.3. n eleman içeren bir sekme listesinin başlangıç düğümleri dışında kalan beklenen düğüm sayısı 2n olur. Kanıt. Belirli bir x elemanının Lr listesi tarafından depolanma olasılığı 1 / 2r, bu nedenle Lr içindeki düğümlerin beklenen sayısı n / 2r olur.8 Böylece, tüm listelerde bulunan düğümlerin beklenen toplam sayısı Önerme 4.4. n eleman içeren bir sekme listesinin beklenen yüksekliği log n + 2 olur. Kanıt. Her r ∈ {1, 2, 3, …, ∞} için gösterge rasgele değişkenini aşağıdaki gibi tanımlayın: Sekme listesi yüksekliği, h, olarak verilir. Unutmayın ki, Ir değeri Lr uzunluğu olan |Lr|’dan asla daha fazla olamaz, bu nedenle, olur. Aşağıdaki hesaplama kanıtı tamamlar: 8 Bunun gösterge değişkenleri ve beklentinin doğrusallığını kullanarak nasıl oluşturulduğunu görmek için bkz. Bölüm 1.3.4. 105 Önerme 4.5. Tüm başlangıç düğümlerinin de içerildiği, n elemanı depolayan bir sekme listesinin beklenen düğüm sayısı, 2n + O(log n) olur. Kanıt. Önerme 4.3 ile belirtilmiştir ki, başlangıç düğümleri dışında kalan düğümlerin beklenen sayısı 2n olur. Başlangıç düğümlerin sayısı sekme listesinin yüksekliği olan, h, ile verilir. Böylece Önerme 4.4 ile belirtilmiştir ki, başlangıç düğümlerinin beklenen sayısı en fazla log n + 2 = O(log n) olur. Önerme 4.6. Bir sekme listesinin beklenen arama yolu uzunluğu en fazla 2 log n + O(1) olur. Kanıt. Bunu görmek için en kolay yol, bir x düğümü için ters arama yolunu ele almaktır. Bu yol L0 içindeki x düğümünden önce gelen düğümle başlar. Zamanla herhangi bir noktada, eğer yol bir seviye yukarı gidebilirse, gider. Bir seviye yukarı çıkamıyorsa o zaman sola doğru gider. Birkaç dakika için düşünülürse, bu ters arama yolunun, ters olması dışında x için arama yolu ile aynı olması konusunda ikna olunur. Belirli bir r seviyesinde, ters arama yolu tarafından ziyaret edilen düğümlerin sayısı aşağıdaki deney ile ilgilidir: Yazı-tura atın. Yazı gelirse, yukarı doğru gidin ve durun. Aksi takdirde, sola doğru hareket edin ve deneyi tekrarlayın. Yazı gelmeden önceki yazı-tura atma sayısı, bir 106 ters arama yolunun, belirli bir seviyede sola doğru attığı adımların sayısını temsil eder. 9 Önerme 4.2 ilk defa yazı gelmeden önceki beklenen yazı-tura atma sayısının 1 olduğunu bildirmiştir. r seviyesinde ileri yöndeki arama yolunun sağa doğru attığı adım sayısının Sr ile gösterildiğini düşünelim. Az önce tartışıldığı gibi, E[Sr] ≤ 1. Ayrıca, Lr içinde Lr uzunluğundan daha fazla adım atamayacağımıza göre Sr ≤ |Lr|, bu nedenle Böylece şimdi, Önerme 4.4.’ün kanıtında olduğu gibi, bitirebiliriz. Sekme listesindeki bir u düğümü için arama yolunun uzunluğu S, ve sekme listesinin yüksekliği h olsun. Bu durumda, 9 Bu sayı sola doğru atılan adım sayısından fazla olabilir, çünkü deney ya ilk defa yazı geldiğinde veya arama yolu başlangıca ulaştığında hangisi önce gelirse bitirilmelidir. Bu bir sorun değildir, çünkü önerme sadece bir üst sınır ifade etmektedir. 107 Aşağıdaki teorem bu bölümdeki sonuçları özetlemektedir: Teorem 4.3. n eleman içeren bir sekme listesinin beklenen boyutu O(n) ve herhangi bir eleman için arama yolunun beklenen uzunluğu en fazla 2 log n + O(1) olur. 4.5 Tartışma ve Alıştırmalar Sekme listeleri Pugh [62] tarafından bir dizi uygulamaları ve uzantıları [61] da dahil olmak üzere tanıtılmıştır. O tarihten bu yana da yoğun çalışmalar yapılmıştır. Çeşitli araştırmacılar i’nci eleman için arama yolunun beklenen uzunluğu ve değişkenleri üzerine çok hassas analizler yapmıştır [45, 44, 58]. Deterministik [53], taraflı [8, 26], ve kendini ayarlayan sekme listeleri [12] geliştirilmiştir. Sekme listeleri uygulamaları çeşitli diller ve çerçeveler için yazılmıştır ve açık kaynak veritabanı sistemlerinde kullanılmaktadır [71, 63]. Sekme listelerinin bir benzeri HP-UX işletim sistemi çekirdeğinin süreç yönetim yapılarında kullanılmaktadır [42]. Sekme listeleri aynı zamanda Java API 1.6 [55] sürümünün bir parçasıdır. Alıştırma 4.1. Şekil 4.1'deki sekme listesi üzerinde 2.5 ve 5.5 elemanları için arama yollarını gösterin. Alıştırma 4.2. Şekil 4.1'deki sekme listesi üzerinde 0.5 (1 yüksekliğe sahip) ve 3.5 (2 yüksekliğe sahip) değerlerinin toplamasını gösterin. Alıştırma 4.3. Şekil 4.1'deki sekme listesi üzerinde 1 ve 3 değerlerinin silinmesini gösterin. Alıştırma 4.4. Şekil 4.5’deki Liste Sekme Listesi üzerinde sil(2) işleminin çalıştırılmasını gösterin. Alıştırma 4.5. Şekil 4.5’deki Liste Sekme Listesi üzerinde ekle(3, x) işleminin çalıştırılmasını gösterin. Varsayın ki, yeni oluşturulan düğüm için yükseklikSeç () fonksiyonu yükseklik değeri olarak 4 seçsin. 108 Alıştırma 4.6. ekle(x) ve sil(x) işlemleri sırasında bir Liste Sekme Listesi içindeki değişen işaretçilerin beklenen sayısının sabit olduğunu gösterin. Alıştırma 4.7. Diyelim ki, bir elemanı yazı-tura atışımızın sonucuna göre Li-1 listesinden Li listesine geçirmek yerine, bunu 0 < p < 1 olan p olasılığı ile gerçekleştiriyoruz. 1. Bu değişiklik ile, bir arama yolunun beklenen uzunluğunun en fazla olduğunu gösterin. 2. Önceki ifadeyi minimize eden p değeri nedir? 3. Sekme listesinin beklenen yüksekliği nedir? 4. Sekme listesinin beklenen düğüm sayısı nedir? Alıştırma 4.8. Sıralı Küme Sekme Listesi bul(x) yöntemi bazen gereksiz karşılaştırmalar gerçekleştirir; x aynı değerle kıyaslandığında bunlar oluşabilir. Bir u düğümü için u.next [r] = u.next [r - 1] olduğunda bunlar oluşur. Bu gereksiz karşılaştırmaların nasıl oluştuğunu gösterin, ve oluşmalarını engellemek için bul(x) gerçekleştiriminde değişiklikler yapın. Değiştirilmiş bul(x) yöntemi tarafından yapılan karşılaştırmaların beklenen sayısını analiz edin. Alıştırma 4.9. Sıralı Küme arayüzünü uygulayan, ama aynı zamanda derecesine göre elemanlara hızlı erişim sağlayan bir sekme listesi versiyonunu tasarlayın ve uygulayın. Bu yapı i dereceli elemanı döndüren al (i) fonksiyonunu O(log n) beklenen zamanda çalıştırmalıdır. (Sıralı Küme’de yer alan bir x elemanının derecesi x değerinin altındaki eleman sayısıdır.) Alıştırma 4.10. Bir sekme listesinde parmak, arama yolunun indiği arama yolu üzerindeki düğüm sırasını depolayan bir dizi olarak tanımlanır. (91. sayfadaki ekle (x) kodundaki yığıt değişkeni bir parmaktır; Şekil 4.3 'deki kırmızı ile boyalı düğümler parmak içeriğini göstermektedir.) Bir parmağın en alttaki, L0 listesindeki bir düğüme ait arama yoluna işaret ettiğini düşünebilirsiniz. Bir parmak araması, u.x < x ve (u.next = null veya u.next.x > x) olan bir u düğümüne ulaşana kadar listeden yukarı doğru yürüyerek parmağı kullanır, ve sonra u düğümünden başlayarak x için normal bir arama yaparak bul (x) işlemini uygular. L0 içinde depolanan x ve 109 parmak ile işaret edilen değer arasındaki sayı değerleri r ise, bir parmak araması için gerekli adımların beklenen sayısının O(1 + log r) olduğunu kanıtlamak mümkündür. Sekme Listesi’nin bir alt sınıfı olan ve içsel bir parmak kullanarak bul (x) işlemini uygulayan Parmak İle Sekme Listesi ’ni gerçekleştirin. Bu alt sınıf, daha sonra her bul (x) işlemini bir parmak araması olarak gerçekleştiren bir parmak depolar. Her bul (x) işlemi sırasında başlangıç noktası olarak, önceki bul (x) işleminin sonucuna işaret eden bir parmak kullanılacak şekilde parmak güncelleştirilmesi yapılır. Alıştırma 4.11. Bir Liste Sekme Listesi’nin i pozisyonunda liste içeriğini kısaltan içeriğiKısalt (i) adında bir yöntem yazın. Bu yöntemi çalıştırdıktan sonra listenin boyutu i olacak şekilde belirlenmeli ve sadece 0, …, i - 1 endekslerindeki elemanları içermelidir. Dönüş değeri i, …, n – 1 endekslerindeki elemanları içeren başka bir Liste Sekme Listesi’dir. Bu yöntemin çalışma zamanı O(log n) olmalıdır. Alıştırma 4.12. l2 argümanında bir Liste Sekme Listesi alan, bu listeyi boşaltarak içeriğini onu çağıran listeye, sırasıyla, ekleyen em(l2) adında bir Liste Sekme Listesi yöntemini yazın. Örneğin, l1 elemanları a, b, c ve l2 elemanları d, e, f olduğu takdirde, l1.em (l2) çağrıldıktan sonra l1 içeriği a, b, c, d, e, f ve l2 içeriği boş olur. Bu yöntemin çalışma zamanı O(log n) olmalıdır. Alıştırma 4.13. Temel yapı olarak bir Sıralı Küme kullanarak (büyük) bir metin dosyasını okuyan ve metinde yer alan herhangi bir alt dize için, etkileşimli, arama yapmanızı sağlayan bir uygulama tasarlayın ve uygulayın. Kullanıcı sorgusunu yazdıkça eş metnin parçası, eğer varsa, sonuçta görünmelidir. İpucu 1: Her altdize bazı sonek’in önekidir. Bu nedenle metin dosyasının tüm sonek’lerini saklamak yeterlidir. İpucu 2: Herhangi bir sonek, kısa ve etkili bir şekilde, ek’in metinde nerede başladığını gösteren bir tamsayı ile tek olarak temsil edilebilir. 110 Gutenberg Projesinde [1] mevcut kitaplardan bazılarında yer alan büyük metinler üzerinde uygulamanızı test edin. Eğer doğru uygulanırsa, tuş vuruşlarını yazarken sonuçları görmek arasında hiçbir gecikme olmayacak şekilde uygulamanız çabuk çalışacaktır. Alıştırma 4.14. (Bu alıştırma Bölüm 6.2’deki ikili arama ağaçları konusu okunduktan sonra yapılmalıdır.) İkili arama ağaçları ile sekme listelerini aşağıdaki şekillerde karşılaştırın: 1. Bir sekme listesinin bazı kenarları silindiğinde, ikili ağaç gibi görünen ve ikili bir arama ağacına benzer bir yapıya bizi nasıl götürdüğünü açıklayın. 2. Sekme listeleri ve ikili arama ağaçlarının her ikisi de yaklaşık aynı sayıda (düğüm başına 2) işaretçi kullanıyor. Oysa ki, sekme listeleri bu işaretçilerden daha iyi yararlanmaktadır. Neden? Açıklayın. 111 Bölüm 5 Karma Tabloları Karma tabloları geniş bir küme olan U = {0, …, 2w - 1 } elemanlarından oluşan küçük sayıda bir n tamsayıyı depolamanın etkili bir yöntemidir. Karma tablosu terimi geniş bir yelpazedeki veri yapılarını içerir. Bu bölümde karma tabloların en yaygın uygulamalarından biri olan zincirleme aracılığıyla karma yöntemi üzerinde duruluyor. Çok sıklıkla karma tabloları tamsayı olmayan veri türlerini depolar. Bu durumda, bir tamsayı karma kodu, her veri elemanı ile ilişkilendirilir ve karma tabloda kullanılır. Bu bölümün ikinci kısmı böyle karma kodların nasıl hesaplandığını anlatıyor. Bu bölümde kullanılan bazı yöntemler, belirli bazı aralıklarda rasgele tamsayılar arasından seçim yapmamızı gerektirir. Verilen kod örneklerinde, bu "rasgele" tamsayılardan bazıları kodlanmış sabitlerdir. Bu sabitler atmosferik gürültüden üretilen rasgele bitler kullanılarak elde edilmiştir. 5.1. Zincirleme Karma Tablo: Zincirleme Aracılığıyla Adresleme Zincirleme Karma Tablo veri yapısı zincirleme aracılığıyla karma yöntemini kullanarak verilerin toplamını bir, t, liste dizisi halinde depolar. Bir tamsayı, n, tüm listelerdeki toplam eleman sayısından sorumludur (bkz. Şekil 5.1): hash(x) ile belirtilen bir x veri elemanının karma değeri, {0, …, t.uzunluk - 1} aralığında bir değerdir. i karma değerine sahip tüm elemanlar listede t[i] pozisyonunda saklanmaktadır. 112 Listelerin çok uzun olmadığından emin olmak için şu değişmez sağlanır: Böylece bu listelerden birinde depolanan elemanların ortalama sayısı n/t.uzunluk ≤ 1 olur. Karma tabloya, bir x elemanı, eklemek için önce t uzunluğunun artırılması gerektiği kontrol edilir, gerekiyorsa artırılır. x karma kodu {0, …, t.uzunluk – 1} aralığında bir i tamsayısı ise, x t[i] listesine eklenir. Gerekirse, tablonun büyültülmesi, t uzunluğunu iki katına çıkarmayı ve yeni tabloya tüm elemanların yeniden eklenmesini içerir. Bu strateji Dizi Yığıt uygulamasında kullanılan strateji ile birebir örtüşür. Sonuç aynıdır: Bir dizi veriyi eklemenin amortize büyüme maliyeti sadece sabit bir sayıdır (bkz. Önerme 2.1, Sayfa 33). Büyültmenin yanı sıra, Zincirleme Karma Tablo’ya yeni bir x değeri eklerken yapılan diğer çalışma sadece t[hash(x)] listesine x eklemeyi içerir. Bölüm 2 veya 3 'de anlatılan Liste uygulamalarının herhangi biri için bu sadece sabit zaman alır. 113 Karma tablodan, bir x elemanını, silmek için önce t[hash(x)] listesi üzerinde ilerleyerek x elemanı bulunur, sonra silinir. Bu O(nhash(x)) zaman alır, ni burada t[i] listesinde depolanmış listenin uzunluğunu ifade ediyor. Karma bir tabloda x elemanının aranması benzerdir. t[hash(x)] listesi içinde doğrusal bir arama gerçekleştiririz: Yine bu, t[hash(x)] listesinin uzunluğu ile orantılı zaman alır. Bir karma tablosunun performansı kritik olarak hash fonksiyonunun seçimi ile ilgilidir. İyi bir karma fonksiyonu elemanları t.uzunluk listeleri arasında eşit olarak dağıtır, böylece t[hash(x)] hash listesinin beklenen boyutu O(n/t.uzunluk) = O(1) olur. Bunun yanı sıra, kötü bir fonksiyonu tüm değerleri (x içinde olmak üzere) aynı tablo konumuna karma yapacaktır, bu durumda, t[hash(x)] listesinin boyutu n olacaktır. Bir sonraki bölümde iyi bir karma fonksiyonunu tanımlıyoruz. 114 5.1.1 Çarpımsal Karma Yöntemi Çarpımsal karma (Bölüm 2.3’de tartışılan) modüler aritmetiğe ve tamsayı bölünmesine dayalı karma değerlerini üreten etkili bir yöntemdir. Bölümün tamsayı kısmını sonuç olarak hesaplayan ve kalanı hesapsız bırakan div operatörünü kullanır. Her tamsayı a ≥ 0 ve b ≥ 1 için a div b = a/b doğru olur. Çarpımsal karma yönteminde d, boyutu belirten bir tamsayı olmak üzere, 2d boyutlu bir karma tablo kullanılır. x ∈ {0, …, 2w – 1} olan bir x tamsayısı için verilen karma formülü olur. Burada z ∈ {1, …, 2w – 1} olan rasgele seçilmiş tek tamsayıdır. Bu karma fonksiyonu tamsayılar üzerindeki işlemlerin varsayılan olarak, w bir tamsayının bit sayısı olmak üzere, zaten modülo 2w ile yapıldığını gözlemleyerek, çok etkin bir şekilde gerçekleştirilebilir (bkz. Şekil 5.2.) Ayrıca tamsayının 2w – d ile bölünmesi ikili tabanda gösterimde en sağdaki w - d bitten kurtulmayı gerektirir. (w – d biti sağa doğru kaydırarak gerçekleştirilebilir.) Böylece, yukarıdaki formülü uygulayan kod formülün kendisinden daha kolay hale gelir: 115 Kanıtı bu bölümün sonlarına kadar ertelenen aşağıdaki önerme, çarpımsal karmanın çatışmalardan kaçınmak gibi iyi bir görevi yerine getirdiğini gösteriyor. Önerme 5.1. {0, …, 2w – 1} içindeki iki ayrı değer x ve y olsun, x ≠ y. Önerme 5.1 ile sil (x) ve bul (x) performanslarını analiz etmek kolaydır: Önerme 5.2. Herhangi bir veri değeri x için, nx karma tabloda x elemanının yinelenme sayısı verildiyse, t[hash(x)] listesinin beklenen uzunluğu en çok nx + 2 olur. Kanıt. x’e eşit olmayan elemanların (çoklu)-kümesi S ile belirtilsin. y ∈ S için gösterge değişkenini tanımlayın. t[hash(x)] Önerme 5.1. ile dikkat edilmelidir ki, listesinin beklenen uzunluğu, gerektiği gibi, olur. Şimdi, Önerme 5.1’i kanıtlamak istiyoruz. Ama ilk olarak sayı kuramının bir sonucuna ihtiyaç duyarız. Aşağıdaki kanıtta, sayısını belirtmek için (br, …, b0)2 gösterimini 116 kullanıyoruz. burada her bi ikili tabanda 0 veya 1 bitleridir. Bir başka deyişle, (br, …, b0)2 ikili tabanda gösterimi br, …, b0 ile verilen bir tamsayıdır. Bilinmeyen değeri belirten bit için kullanıyoruz. Önerme 5.3. {1, …, 2w – 1} kümesindeki tek tamsayıların kümesi S olsun; q ve i S içinde herhangi iki eleman olsun. Bu durumda, z ∈ S iken eşitliğini sağlayan sadece bir değer tam olarak vardır. Kanıt. z ve i için eşit sayıda seçim yapılabileceği için eşitliğini sağlayan en fazla bir z ∈ S değerinin var olduğunu kanıtlamak yeterlidir. Bir çelişki denemek için, z > z’ olmak üzere iki ayrı z ve z’ değeri olduğunu varsayalım. Bu nedenle, Bu eşitlik aynı şekilde k tamsayısı için yeniden yazılır: İkili tabanda gösterilen sayılar açısından düşünüldüğünde, Böylece (z – z’) q sayısının ikili gösteriminde sondan w bit 0 olur. Ayrıca k ≠ 0 ve z – z’ ≠ 0 olduğu için q ≠ 0 doğrudur. q tek sayı olduğundan ikili tabanda gösteriminin son biti 0 olamaz: 117 z – z’ < 2w olduğu için, z – z’ ikili tabanda gösterimi w bitten daha az sayıda 0 bite sahiptir: Bu nedenle, (z – z’)q çarpımının ikili tabanda gösterimi w bitten daha az sayıda 0 bite sahiptir: Bu nedenle, (z – z’)q (5.1) eşitliğini sağlamaz, çelişki olur ve kanıt tamamlanır. Şu gözlemden yararlanırsak, Önerme 5.3 daha iyi anlaşılacaktır: S içinden Z rasgele seçilirse, zt’nin S üzerindeki dağılımı normaldir. Aşağıdaki kanıtta size yardımcı olabilecek bir nokta, ikili tabanda z gösterimini w – 1 rasgele bitin ardından gelen 1 biti olarak düşünmek olacaktır. Önerme 5.1. Kanıtı: Öncelikle unutmayın ki, hash(x) = hash (y) koşulu “zx mod 2w sonucunun en-yüksek dereceden d biti ve zy mod 2w sonucunun en-yüksek dereceden d biti aynıdır.” ifadesine eşdeğerdir. Bu açıklamanın gerekli koşulu z(x – y) mod 2w sayısının ikili tabanda gösteriminin en yüksek dereceden d biti ya hep 0 ya hep 1 olmalıdır. zx mod 2w > zy mod 2w koşulu ile birlikte veya zx mod 2w < zy mod 2w koşulu ile birlikte 118 gösterilir. Bu nedenle, sadece z(x – y) mod 2w olasılığını (5.2) veya (5.3)’teki bağıntılarla ilişkilendirmek zorundayız. q tek olan tekil tamsayı olsun, öyle ki bazı r tamsayısı için . Önerme 5.3 sayısının ikili tabanda gösterimi w – 1 rasgele bit, ardından 1 bit içerir : ile Bu nedenle, sayısının ikili tabanda gösterimi w – r – 1 rasgele bit, ardından 1 bit içerir : Kanıtı şimdi bitirebiliriz: Eğer r > w – d ise, sayısının en yüksek dereceden d sayısının (5.2) veya (5.3) gibi biti hem 0 ve hem 1’leri içerir, böylece görünme olasılığı 0’dır. Eğer r = w – d ise (5.2) gibi olma olasılığı 0’dır, ancak (5.3) gibi görünme olasılığı 1/2d-1 = 2/2d olur (çünkü b1, …, bd-1 = 1, …, 1 doğrudur). Eğer r < w – d ise, bw – r – 1, …, bw – r – d = 0, …, 0 veya bw – r – 1, …, bw – r – d = 1, …, 1 doğrudur. Bu durumların her birinin olasılığı 1 / 2d ve ayrışık oldukları için her iki durumun olasılığı 2 / 2d olur. 5.1.2 Özet Aşağıdaki teorem Zincirleme Karma Tablo veri yapısının performansını özetler: Teorem 5.1. Zincirleme Karma Tablo, Sırasız Küme arayüzünü uygular. yeniden_boyutlandır () için yapılan çağrıların maliyeti dikkate alınmayarak, Zincirleme Karma Tablo işlem başına O(1) beklenen zamanda çalışan ekle(x), sil(x) ve bul(x) işlemlerini destekler. 119 Bundan başka, boş bir Zincirleme Karma Tablo oluşturulursa ve ekle(x) ve sil (x) işlemleri m ≥ 1 defa çağrılırsa, yeniden_boyutlandır () çağrıları için gerekli toplam çalışma süresi O(m) olur. 5.2. Doğrusal Karma Tablo: Doğrusal Yerleştirme Doğrusal Karma Tablo veri yapısında, i’nci listede hash(x) = i olan tüm x elemanlarının depolandığı bir liste dizisi kullanılmaktadır.. Alternatif olarak, açık adresleme ile elemanlar doğrudan, t, dizisi içinde ve her t konumunda en fazla bir değer olacak şekilde depolanabilir. Bu yaklaşım, bu bölümde anlatılan Doğrusal Karma Tablo tarafından uygulanmıştır. Bazı yerlerde, bu veri yapısı doğrusal yerleştirme ile açık adresleme olarak tanımlanmaktadır. Bir Doğrusal Karma Tablo ardındaki ana fikir x elemanını hash(x) = i hash değeri ile ideal olarak t[i] tablo konumunda saklamak istememiz yatıyor. Zaten orada var olan bir eleman saklandığı için bu yapılamıyorsa, o zaman t[(i + 1) mod t.uzunluk] konumunda saklanması istenir. Bu mümkün değilse, t [(i + 2) mod t.uzunluk] konumu denenir, ve böylece, x için bir yer bulunana kadar devam edilir. t içinde saklanan üç tür girdi vardır: 1. veri değerleri: Sırasız Küme’de depolanan gerçek değerler. 2. boş (null) değerler: Hiçbir veri kaydı yapılmamış dizi konumlarındaki değerler. 3. del değerler: Verilerin bir zamandan bu yana dizi konumlarında saklandığı ancak silindiği değerler. n eleman sayısının yanı sıra, q ile 1.tür ve 3.tür elemanların sayısı belirtilir. Bunun anlamı, q n ve del değerlerini taşıyan elemanların toplam sayısı olur. Bu işi verimli hale getirmek için, t için ayrılan üst limitin q değişkeninde tutulan değerden nispeten çok büyük olması gerekir. Böylece t içinde ayrılmış pek çok boş değer yeri olmalıdır. Bu nedenle, Doğrusal Karma Tablo üzerinde yapılan işlemler için t.uzunluk ≥ q değişmezi tanımlanabilir. 120 Özetlemek gerekirse, veri elemanlarını depolamak için Doğrusal Karma Tablo bir t dizisi kullanır, n ve q tamsayıları veri elemanlarının sayısını ve boş-olmayan t değerlerini belirtir. Birçok karma fonksiyonları sadece 2 üssündeki bir sayıya kadar eleman depolayan tablo boyutlarına kadar çalıştıkları için bir d tamsayısı tanımlayabilir ve t.uzunluk = 2d değişmezini koruyabiliriz. Bir Doğrusal Karma Tablo içinde bul (x) işlemi basittir. hash(x) = i olan t[i] dizi pozisyonuyla başlarız. Sırasıyla t[i], t [(i + 1) mod t.uzunluk], t [(i + 2) mod t.uzunluk] pozisyonları aranır, ve böylece, t[i’] = x, ya da t[i’] = null koşullarından en az birini sağlayan bir i’ konumu bulunana kadar arama devam eder. Bir önceki durum sağlanmışsa, t[i’] elemanı döndürülür. Sonraki durum sağlanmışsa, karma tabloda x elemanının yer almadığı sonucuna varılır ve boş (null) değer döndürülür. ekle(x) işlemini uygulamak da oldukça kolaydır. Tabloda x elemanının henüz depolanmadığını (bul(x) ile) kontrol ettikten sonra, boş (null) veya del değerleri bulunana kadar t[i], t[(i + 1) mod t.uzunluk], t[(i + 2) mod t.uzunluk] pozisyonları aranmaya devam eder. x tam da o pozisyona dahil edilir, q ve n sayıları uygun şekilde güncellenir. 121 Şimdiden sonra, sil(x) işlemini uygulamak açık hale geldi. t[i’] = x veya t[i’] = null koşulunu sağlayan bir i’ endeksi bulunana kadar t[i], t[(i + 1) mod t.uzunluk], t[(i + 2) mod t.uzunluk] pozisyonları aranmaya devam eder. Bir önceki durum sağlanmışsa, t[i0] = del olarak belirlenir ve true döndürülür. Sonraki durum sağlanmışsa, karma tabloda x elemanının yer almadığı sonucuna varılır (ve bu nedenle silinemez) ve false döndürülür. bul (x), ekle (x) ve sil (x) yöntemlerinin doğrulanması del değerlerinin kullanımına bağlı da olsa kolaydır. Bu işlemlerin hiçbiri dolu veya del girişlere boş (null) değer atamaz. Bu nedenle, t[i’] = null olan i’ endeksine ulaşmak mutlak şekilde x tabloda yer almıyor ve yine aynı anlamda t[i’] bundan önce de her zaman (boş) null değere sahipti ve yine aynı şekilde öncesinde çalıştırılan ekle (i’) işlemleri dizide en fazla i’ pozisyonuna kadar gittiği, bir başka deyişle i’ pozisyonundan ileri gitmediği (i’+1, i’+2 …) anlamına gelir. 122 yeniden_boyutlandır () yöntemi boş (null) olmayan girişlerin sayısı t.uzunluk / 2 sayısından fazla hale geldiği zaman ekle (x) tarafından ve t.uzunluk / 8 sayısından az hale geldiği zaman sil (x) tarafından çağrılır. yeniden_boyutlandır () yöntemi diğer dizi-tabanlı veri yapılarında çalıştırılan yeniden_boyutlandır () yöntemi gibi çalışır. Negatif olmayan en küçük d tamsayısı 2d ≥ 3 n bulunur, t dizisi 2d boyutuna sahip olacak şekilde yeniden tahsis edilir, ve sonra eski dizide yer alan tüm elemanlar yeni boyutlu t dizisi içine tekrar yerleştirilir. Bunu yaparken, q eşittir n olarak belirlenir, çünkü yeni dizide hiçbir eleman henüz silinmemiştir (del = 0 doğrudur). 5.2.1 Doğrusal Yerleştirme Analizi Unutmayın ki ekle (x), sil (x) ve bul (x) işlemleri için t dizisinde yer alan ilk boş (null) giriş bulunduğu zaman (veya bu endeksten öncesinde de olabilir) doğru pozisyon bulunmuş anlamına gelir. Doğrusal yerleştirme analizinde mantık t elemanlarının en az yarısı boştur, bir işlem tamamlaması çok sürmez çünkü hızlı bir şekilde boş (null) girişe ulaşılır. Ancak bu mantık çok kuvvetli değildir, çünkü bize sadece t boyutu hakkında (yanlış) ipucu verir. Burada önemli olan, n, q, ve del arasındaki rastlantısal bağları kuvvetlendirerek onları bu değişkenler cinsinden yeniden boyutlandırma olasılığı ile ilişkilendirmektir. 123 Bu bölümün geri kalanı için, hepimiz karma değerlerin birbirinden bağımsız olduğunu ve {0, …, t.uzunluk – 1 } aralığına normal olarak dağıtılmış olduğunu kabul eder. Bu gerçekçi bir varsayım değildir, ama bize doğrusal yerleştirmeyi analiz etmek için mümkün bir başlangıç noktası sağlar. Bir sonraki aşama, “Listeleme Karma” adında diğer yöntemi tanıtıyor. Doğrusal yerleştirme için “en uygun” karma fonksiyonu ayrıştırabilen akıllı bir seçenektir. Bu analiz sırasındaki varsayımımız, t dizisindeki endekslerin tümü için t.uzunluk modüler aritmetiğinin sonucuna göre pozisyon belirlendiği olmuştur (t[i] = t[i mod t.uzunluk]). i pozisyonundan başlayan k uzunluğundaki bir çalıştırma adımından bahsediyorsak tüm dizi girişleri t[i], t[i + 1], … , t[i + k – 1] null olmadığı takdirde ve aynı zamanda t[i – 1] = t[i + k] = null ise, gerçekleşir. Boş (null) olmayan t elemanlarının sayısı tam olarak q ile ölçülür, ekle (x) yöntemi her zaman q ≤ t.uzunluk / 2 sağlar. Şu anki bulunduğumuz zamandan öncesinde diziye q eleman, x1, …, xq eklenmişti. q değeri yeniden_boyutlandır () işleminin en son çalıştırıldığı anki tabloya işaret eder. Varsayımımız şudur ki tablo elemanlarının her biri karma değer taşımaktadır. Bu değeri hash(xj) ile belirtiyoruz. hash(xj) değerleri normal dağılımlı ve birbirinden bağımsızdır. Bu açıklama ile doğrusal yerleştirmeyi analiz etmek için gerekli önermeyi kanıtlamaya geçebiliriz. Önerme 5.4. i ∈ {0, …, t.uzunluk – 1} olsun. 0 < c < 1 sabiti ile bir işlemin k uzunluğunda i pozisyonundan başlayarak çalışma zaman O(ck) olur. Kanıt. Bir işlem i pozisyonundan başlayarak k uzunluğunda çalışırsa, hash(xj) ∈ {i, …, i + k – 1} olan eleman sayısı k olur. Bu durumun tam olarak olasılığı çünkü her k elemanı için tablo üzerinde doğru pozisyonu bulmak, bu elemanların k konumundan birine karma edilmesini ön şart kılar, ve kalan q – k eleman t.uzunluk – k tablo pozisyonu için ayrılır.10 10 Bu tanım k uzunluğunun i pozisyonundan çalışmaya başlamasından büyük bir sayı sonucu üretir, çünkü t[i – 1] = t[1 + k] = null. 124 Aşağıdaki gösterimde faktoriyel işlemler için Stirling'in Yaklaşımı’na (Bölüm 1.3.2) göre hata payı getiren r! ≈ (r/e)r yer değiştirmesi yapacağız. Bu sadece çıkarımı kolaylaştırmak için yapılmıştır. Alıştırma 5.4 bütünüyle Stirling’in Yaklaşımı kullanılarak titiz bir hesaplama mantığı getiriyor. t.uzunluk ≥ 2q t.uzunluk koşulu sağlanarak şekilde pk değerini en yüksek derecede düşündüğünüzde, değerinin en küçük olması gerektiğine karar verirsiniz, bu nedenle (Son adım olarak, her x > 0 için doğruluğu tanım ile sabitlenen (1 + 1/x)x ≤ kullanacağız.) e eşitsizliğini olduğundan kanıt tamamlanır. Şimdi açık hale gelmiş olmalıdır ki bul (x), ekle (x) ve sil (x) işlemlerinin beklenen çalışma zamanları Önerme 5.4 kullanarak kolay hale gelmiştir. Doğrusal Karma Tablo’nun depolanmayan bazı x değerleri için bul (x) işleminin çalıştırıldığı en kolay bir durumu düşünün. Bu durumda, i = hash(x) değeri {0, t.uzunluk – 1} içinde t içeriğinden bağımsız ve rasgeledir. k uzunluğunda bir çalışmanın i kısmı için bul (x) işleminin çalışma zamanı en çok O(1 + k) olur. Böylece, beklenen çalışma zamanı üst sınırı 125 Unutmayın ki, k uzunluğu için her çalışmada k artırılır. k2 dersek, yukarıdaki ifade tekrar yazılır: katlanarak azalan dizidir.11 Bu Çıkarımın sonunda varsayımda bulunduk. nedenle, bul (x) işleminin beklenen çalışma zamanı tabloda yer almayan x değeri için O(1) olur. yeniden_boyutlandır() Tablo maliyeti görmezden gelinerek, yukarıdaki analiz Doğrusal Karma işlemlerinin tümü için gerekli sonuçları çıkarmamıza olanak sağlar. Birincisi, bul(x) analizinin ekle(x) işlemine uygulanması ancak x tabloda yer almadığı süre için geçerlidir. x tabloda ise bul(x) işlemini analiz etmek bize x tabloda yer almadığı takdirde ekle(x) için yapılan analiz ile maliyet açısından aynı sonucu üretir. Fonksiyonel düşünüldüğünde sil(x) işlemi de en üst seviyede bul(x) ile sınırlıdır. Özet olarak, yeniden_boyutlandır () için yapılan çağrıların maliyeti dikkate alınmayarak Doğrusal Karma Tablo 11 işlem başına O(1) beklenen zamanda çalışan işlemleri destekler. Dizi Birçok ders kitabında yazılıdır ki koşulunu sağlayacak bir k0 pozitif tamsayısı k ≥ k 0 için vardır. 126 Yığıt için yapılan yeniden_boyutlandır () analizi amortize zamanda Doğru Karma Tablo için geçerlidir. 5.2.2 Özet Aşağıdaki teorem Doğrusal Karma Tablo veri yapısının performansını özetler: Teorem 5.1. Doğrusal Karma Tablo, Sırasız Küme arayüzünü uygular. yeniden_boyutlandır () için yapılan çağrıların maliyeti dikkate alınmayarak, Doğrusal Karma Tablo işlem başına O(1) beklenen zamanda çalışan ekle(x), sil(x) ve bul(x) işlemlerini destekler. Bundan başka, boş bir Doğrusal Karma Tablo oluşturulursa ve ekle(x) ve sil (x) işlemleri m ≥ 1 defa çağrılırsa, yeniden_boyutlandır () çağrıları için gerekli toplam çalışma süresi O(m) olur. 5.2.3 Listeleme Karma Yöntemi Doğrusal Karma Tablo’yu {x1, …, xn} analiz ederken, çok güçlü bir varsayım yapılmıştır: Her elemanlarının hash(x1), …, hash(xn) karma değerleri {0, …, t.length – 1} üzerine bağımsız olarak ve normal dağılmıştır. Bunu yapmanın bir yolu, her girişin tüm diğer kayıtlardan bağımsız olduğu rasgele bir w-bit tam sayı depolayan 2w boyutunda çok büyük bir tab dizisi saklamaktır. Bu şekilde, tab[x.hashCode()] girişinden d-bit tam sayıyı ayıklayarak hash(x) fonksiyonunu gerçekleştirebildik. Ne yazık ki, 2w boyutunda bir dizi depolamak bellek kullanımı açısından engelleyicidir. Bunun yerine, Listeleme Karma yöntemi tarafından kullanılan yaklaşım, w-bit tamsayıyı 127 sadece r bite sahip w/r tamsayı tarafından oluşmuş gibi işlemektir. Böylece, Listeleme Karma sadece uzunluğu 2r olan w/r diziye ihtiyaç duyar. Bu dizilerdeki tüm kayıtlar bağımsız w-bit tamsayılarından bağımsızdır. hash(x) karma değerini elde etmek için x.hashCode() değerini w/r adet r-bit tamsayıya böleriz ve bunları bu diziler içine yapılan endeksler gibi kullanırız. Daha sonra hash(x) karma değerini elde etmek için tüm bu değerleri bitsel veya-hariç operatörü ile birleştiririz. Aşağıdaki kod, w = 32 ve r = 4 olduğu zaman bunun nasıl çalıştığını gösterir. Bu durumda, tab, dört sütunlu ve 232/4 = 256 satırlı iki boyutlu bir dizidir. Kolayca doğrulayabilirsiniz ki, her x için hash(x) değeri {0, ..., 2d - 1} üzerine normal dağılmıştır. Küçük bir çalışma ile, herhangi iki karma değerinin birbirimden bağımsız olduğunu da doğrulayabilirsiniz. Bunun anlamı Listeleme Karma’nın Zincirleme Karma Tablo gerçekleştirimi için Çarpımsal Karma yöntemi yerine kullanılabileceğini ifade eder. Ancak, herhangi bir küme ile verilen n farklı elemanın n bağımsız hash değeri ürettiği doğru değildir. Bununla birlikte, listeleme karma yöntemi kullanıldığında, Teorem 5.2 hala geçerlidir. Buna ait referanslar bu bölümün sonunda verilmiştir. 5.3. Karma Kodları Önceki bölümde tartışılan karma tabloları w bitten oluşan tamsayı anahtarlarını veri ile ilişkilendirmek için kullanıldı. Oysa ki, birçok durumda, tamsayı olmayan anahtarları ile karşılaşırız. Anahtarlarımız dizeler, nesneler, diziler ya da diğer bileşik yapılar olabilir. Bu 128 veri türleri için karma tablolarını kullanmak için, bu veri türlerini w-bit karma kodları ile eşleştirmemiz gerekir. Karma kodu eşleştirmeleri aşağıdaki özelliklere sahip olmalıdır: 1. x ve y eşit ise, x.hashCode() ve y.hashCode() eşittir. 2. x ve y eşit değil ise, x.hashCode() = y.hashCode() olasılığı küçük olmalıdır (yaklaşık 1/2w). Birinci özellik eğer karma bir tabloda x elemanını depoluyorsak ve daha sonra x’e eşit olan bir y değerine bakıyorsak, o zaman – gerektiği gibi x’i buluruz koşulunu sağlar. İkinci özellik nesneleri tamsayılara dönüştürmemizden gelen kaybı en aza indirir. Eşit olmayan nesnelerin genellikle farklı karma kodları vardır ve böylece karma tablomuzda farklı yerlerde depolanmış olmaları muhtemeldir koşulunu sağlar. 5.3.1 Temel Veri Türleri için Karma Kodları char, byte, int ve float gibi küçük temel veri türleri için karma kodlarını bulmak genellikle kolaydır. Bu veri türlerinin her zaman ikili gösterimi vardır ve bu ikili gösterim genellikle w veya daha az bitten oluşur. (Örneğin, Java’da byte 8-bitlik bir türdür, ve float 32-bitlik türdür.) Bu durumlarda, biz sadece bu bitleri {0, …, 2w – 1} aralığında bir tamsayının temsili olarak görüyoruz. İki değer farklı ise, farklı karma kodları olur. Aynı iseler, aynı karma kodu olur. Birkaç temel veri türü w bitten daha fazlasından, ya da bir tamsayı sabiti c için genellikle cw bitten oluşur.(Java long ve double türleri c = 2 ile bunun örnekleridir.) Bir sonraki bölümde anlatıldığı gibi, bu veri türleri c bölümden oluşan bileşik nesneler olarak kabul edilebilir. 5.3.2 Bileşik Nesneler için Karma Kodları Bir bileşik nesne için nesneyi oluşturan parçaların tek tek karma kodlarını birleştirerek bir karma kodu oluşturmak istiyoruz. Bu göründüğü kadar kolay değildir. Bunu becermek için pek çok çözüm yolu bulabileceğinize rağmen (örneğin, bitsel veya–hariç işlemleri ile karma 129 kodları birleştirerek) bu yollardan çoğu kolayca bozuk hale gelir (bkz Alıştırmalar 5.7 – 5.9) Ancak, 2w bit duyarlılığında aritmetik yapmaya istekli iseniz, o zaman mevcut basit ve sağlam yöntemler vardır. Birkaç parçadan, P0, ..., Pr-1, oluşan bir nesne olduğunu varsayalım. Bu parçaların karma kodları, sırasıyla, x0, …, xr-1 olsun. O zaman karşılıklı bağımsız rasgele w-bit tamsayılar, z0, …, zr-1, ve rasgele 2w-bit tek tamsayı z seçebilir ve nesnemiz için bir karma kodu hesaplayabiliriz: Unutmayın ki, bu hash kodunun 2w-bitlik ara sonucu alarak bunu bir w-bitlik final sonuç haline getirmek için Bölüm 5.1.1’de verilen çarpımsal karma formülünü kullanan (z ile çarpan ve 2w ile bölen) bir son adımı vardır. Burada bu yöntemi, 3 parçadan, x0, x1, x2, oluşan basit bir bileşik nesneye uygulamanın bir örneği verilmiştir. Aşağıdaki teorem göstermektedir ki, uygulamasının basit olmasının yanı sıra, bu yöntem kanıtlanabilirlik açısından da iyidir. Teorem 5.3. Her x0, …, xr–1 ve y0, …, yr–1 tamsayısı {0, …, 2w – 1} içinde yer alan w bit birer dizi olsun ve i ∈ {0, …, r – 1} olan en az bir i endeksi için varsayın ki, xi ≠ yi . O zaman, 130 Kanıt. En sondaki çarpımsal karma adımını görmezden geleceğiz ve bu adımın daha sonra nasıl katkıda bulunacağına bakacağız. Tanımlayın: Diyelim ki, h’(x0, …, xr–1) = h’(y0, …, yr–1). Bunu şöyle yeniden yazabiliriz: Burada, olur. Genelleştirme kaybı olmaksızın xi > yi kabul edersek, o zaman her bir zi ve (xi – yi) en fazla 2w – 1 olduğu, ve bu nedenle çarpımları en fazla 22w – 2w+1 + 1 < 22w – 1 olacağı için (5.4) haline gelir. Varsayım olarak, xi – yi ≠ 0, bu nedenle (5.5)’in zi içinde en çok bir çözümü vardır. zi ve t bağımsız olduğundan (z0, …, zr – 1 karşılıklı bağımsızdır), h’(x0, …, xr–1) = h’(y0, …, yr–1) koşulunu sağlayan bir zi seçme olasılığımız en fazla 1/2w olur. Karma fonksiyonunun son adımı çarpımsal karma yöntemini uygulayarak 2w bitlik h’(x0, …, xr–1) ara sonucunu, son sonuç olan w bitlik h(x0, …, xr–1) haline getirmektir. Teorem 5.3 ile, eğer h’(x0, …, xr–1) ≠ h’(y0, …, yr–1) ise, Pr{h(x0, …, xr–1) = h(y0, …, yr–1)} ≤ 2/2w olur. Özetlemek gerekirse, 131 5.3.3 Diziler ve Dizeler için Karma Kodları Daha önceki bölümde elde edilen yöntem değişmez, sabit sayıda bileşenler içeren nesneler için iyi çalışır. Ancak değişken sayıdaki bileşenler içeren nesneler ile birlikte kullanmak istiyorsak, her bileşen için rasgele bir w-bit tamsayı zi gerektirdiğinden bu yöntem bozulur, kullanılamaz. İhtiyacımız kadar zi oluşturmak için bir sözde rasgele dizisi kullanabilirdik ama o zaman zi 'lar birbirinden bağımsız olmazdı ve sözde rasgele sayıların bizim kullandığımız karma fonksiyonu ile kötü etkileşimde olmadığını kanıtlamak zor hale gelirdi. Özellikle, Teorem 5.3 kanıtındaki t ve zi değerleri artık bağımsız değildir. Daha titiz bir yaklaşımla, karma kodları asal katsayılara sahip polinomlara dayandırabiliriz; bunlar bir, p, asal sayısı için modülo p üzerinden değerlendirilen normal polinomlardır. Bu yöntem, asal katsayılı polinomların hemen hemen her zamanki polinomlar gibi davrandığını söyleyen aşağıdaki teoreme dayanır: Teorem 5.4. p bir asal sayı olsun, ve önemsiz-olmayan bir polinomun, f (z) = x0z0 + x1z1 + … + xr – 1 zr - 1, katsayıları xi ∈ {0, …, p – 1} f (z) mod p = 0 olsun. Bu durumda, denkleminin z ∈ {0, …, p – 1} için en fazla r – 1 çözümü bulunur. Teorem 5.4’ü kullanmak için her xi ∈ {0, …, p – 2} olan bir x0, …, xr–1 tamsayı dizisini rasgele bir z tamsayısı, z ∈ {0, …, p – 1}, kullanarak aşağıdaki formülle karma ederiz. Formülün sonundaki ek terime, (p-1)zr dikkat edin. Bu terim x0, …, xr dizisinin son elemanı xr olarak (p–1)’i düşünmemize yardımcı olur. Unutmayın ki, bu eleman dizideki diğer her 132 elemandan (her biri {0, …, p – 2} kümesinde olan) farklıdır. p–1 bir dizi-sonu-belirteci olarak da düşünülebilir. Aynı uzunluktaki iki dizi durumunu ele alan aşağıdaki teorem bu karma fonksiyonunun z seçmek için ihtiyaç duyulan az miktarda rasgelelik için iyi bir getiri sağladığını göstermektedir. Teorem 5.5. p > 2w + 1 asal sayı olsun. Her x0, …, xr–1 ve y0, …, yr–1 tamsayısı {0, …, 2w – 1} içinde yer alan w bit birer dizi olsun ve i ∈ {0, …, r – 1} olan en az bir i endeksi için varsayın ki, xi ≠ yi . Bu durumda, Kanıt. h(x0, …, xr–1) = h(y0, …, yr–1) denklemi yeniden şöyle yazılır: x1 ≠ y1 olduğundan bu polinom önemsiz-değildir. Bu nedenle, Teorem 5.4 ile, z içinde en fazla r – 1 çözümü bulunur. Bu çözümlerden biri olarak z seçilmesinin olasılığı, bu nedenle en fazla, (r – 1)/p olur. Unutmayın ki bu karma fonksiyonu, aynı zamanda, iki dizinin farklı uzunluklara sahip olduğu durum ve dizilerin biri diğerine önek olduğu zaman olsa bile ilgilidir. Bunun nedeni, bu fonksiyonun etkin şekilde aşağıda belirtilen sonsuz diziye karma sağlamasıdır. r > r’ olan r ve r’ uzunluğunda iki dizi varsa, o zaman bu iki dizi i = r endeksinde farklılık gösterir. Bu durumda, (5.6) Teorem 5.4 ile z içinde en fazla r çözümü bulunan 133 haline gelir. Teorem 5.5 ile birleştirildiğinde aşağıdaki daha genel teoremi kanıtlamaya yeterli oluruz. Teorem 5.6. p > 2w+1 asal sayı olsun. Her x0, …, xr–1 ve y0, …, yr’–1 tamsayısı {0, …, 2w – 1} içinde yer alan w bit birer dizi olsun. Bu durumda, Aşağıdaki örnek kod, bu karma fonksiyonun bir x dizisi içeren bir nesneye nasıl uygulandığını gösterir: Yukarıdaki kod uygulamada kolaylık sağlamak için bazı çarpışma olasılığından fedakarlık etmiştir. Özellikle, d = 31 ile x[i].hashCode() karma kodunu 31-bit değere indirmek için Bölüm 5.1.1’deki çarpımsal karma fonksiyonunu uygular. Bu sayede, modülo p = 232 – 5 asal sayısı ile yapılan eklemeler ve çarpmalar 63-bit işaretsiz aritmetiği kullanılarak gerçekleştirilebilir. Böylece daha uzun olanın r uzunlukta olduğu iki farklı dizinin aynı karma koduna sahip olma olasılığı, Teorem 5.6 ‘da belirtilen r / (232 – 5) aksine olur. 134 5.4 Tartışma ve Alıştırmalar Karma tabloları ve karma kodları bu bölümde sadece üzerine dokunulan bir araştırmanın büyük ve aktif alanını temsil eder. Karma [10] üzerine yaklaşık 2000 girdi içeren çevrimiçi Bibliyografya’ya erişebilirsiniz. Farklı karma tablo uygulama çeşitleri mevcuttur. Bölüm 5.1'de anlatılan yöntem zincirleme ile karma olarak bilinir. Her dizi girişi bir zincir (Liste) içindeki elemanları içerir. Zincirleme ile karma tarihi Ocak 1953’te H.P.Luhn tarafından hazırlanan bir IBM iç bildirisine kadar uzanır. Bu bildiri bağlı listelere de ilk referanslardan biri olarak görünüyor. Zincirleme ile karmaya bir alternatif olarak, tüm verilerin bir dizi içine doğrudan depolandığı, açık adresleme şemaları tarafından kullanılan karmalardır. Bu şemalar, Bölüm 5.2’deki Doğrusal Karma Tablo yapısını içerir. Bu fikir aynı zamanda bağımsız olarak 1950’lerde, IBM’deki bir grup tarafından önerilmişti. Açık adresleme şemaları çarpışma çözülümü sorunu ile ilgilenmek zorundadır: iki değerin aynı dizi konumuna karma olması durumuyla. Çarpışma çözümü için farklı stratejiler için vardır; bunlar farklı performans garantisi sağlar, ve burada sık sık anlatılmış olanlardan daha sofistike karma fonksiyonlar gerektirir. Yine karma tablo uygulamalarının başka bir kategorisi mükemmel karma yöntemler diye anılan yöntemlerdir. Bu yöntemler bul(x) işleminin en kötü O(1) zamanda çalıştığı yöntemlerdir. Statik veri kümeleri için her veri parçasını benzersiz bir dizi konumuna eşleştiren mükemmel karma fonksiyonları bulunarak bu gerçekleştirilebilir. Zamanla değişen veriler için, mükemmel karma yöntemleri şunlardır FKS iki-seviyeli karma tablosu [31, 24] ve guguk-kuşu karma yöntemi [57]. Bu bölümde sunulan hızlı arama fonksiyonları muhtemelen herhangi bir veri seti için iyi çalışacağı kanıtlanır olabilen şu anda bilinen en pratik yöntemler arasındadır. Diğer kanıtlanabilir iyi yöntemler Carter ve Wegman’ın evrensel karma kavramını tanıtan ve farklı senaryolar için çeşitli karma fonksiyonları anlatan öncü çalışmalarına [14] kadar uzanmaktadır. Bölüm 5.2.3 'de anlatılan listeleme karma, Carter ve Wegman tarafından açıklanmıştır [14], ancak doğrusal karma tabloya (ve diğer birçok karma tablo şemalarına) uygulanmış haliyle analizini Patraşcu ve Thorup’a [60] borçluyuz. 135 Çarpımsal karma fikri çok eskidir, ve karma folklor parçası gibi görünüyor [48, Bölüm 6.4]. Ancak, z çarpanını rasgele tek sayı olarak seçme fikri ve Bölüm 5.1.1’de anlatılan analizi Dietzfelbinger et al. [23] tarafından tartışılmıştır. Çarpımsal karmanın bu sürümü en basitlerinden biridir, ancak ikinin bir faktörü olan çarpışma olasılığı, 2/2d, 2w → 2d ile belirtilen bir rasgele fonksiyondan beklediğinizden çok büyüktür. Çarpma-toplama karma yöntemi z ve b rastgele olarak {0, …, 22w – 1} içinden seçilen fonksiyonunu kullanır. Çarpma-toplama karmasının sadece 1/2d [21] çarpışma olasılığı vardır, ancak 2w-bit duyarlı aritmetik gerektirir. Sabit uzunlukta olan w-bit tamsayı dizilerinden karma kodlar elde etmenin yöntemleri vardır. Özellikle hızlı bir yöntem, çift sayı olan bir r ve {0, …, 2w} içinden rasgele seçilen a0, …, ar–1 değerleri için [11] ile verilen fonksiyonudur. Bu 1/2w çarpışma olasılığı olan 2w-bit hash kodu verir. Bu çarpımsal karma (veya çarpma-toplama) yöntemi kullanılarak w-bit hash koduna indirilebilir. bu yöntem hızlıdır, çünkü sadece r/2 2w-bit çarpma gerektirir; oysa ki Bölüm 5.3.2’de anlatılan yöntem r çarpma gerektiriyordu. (mod işlemleri sırasıyla eklemeler ve çarpım için w ve 2w-bit aritmetik kullanarak, dolaylı olarak meydana gelir.) Bölüm 5.3.3’te anlatılan, asal katsayılı polinomların değişken uzunluktaki diziler ve dizelere karma edilerek kullanılması yöntemi, Dietzfelbinger et al. [22] tarafından getirilmiştir. Pahalı bir makine komutuna dayanan mod operatörünü kullanması nedeniyle, ne yazık ki, çok hızlı değildir. Bu yöntemin bazı çeşitleri asal p olarak 2w – 1 seçer. Bu durumda mod operatörü ekleme (+) ile ve bitsel-ve (&) işlemi [47, Bölüm 3.6] ile değiştirilebilir. Başka bir seçenek sabit uzunluktaki dizeler için mevcut hızlı yöntemlerden birini c > 1 sabitiyle verilmiş c 136 uzunluğundaki bloklara uygulamak ve sonra asal katsayı yöntemini r/c karma kodu sonuç dizisi için uygulamaktır. Alıştırma 5.1. Belirli bir üniversite her öğrencisine herhangi bir kurs için ilk kez kayıt oldukları zaman birer öğrenci numarası atıyor. Yıllar önce 0 ile başlayan bu ardışık tamsayılar şimdi milyonları bulmuştur. Yüz adet birinci sınıf öğrencisi olduğunu ve onların öğrenci sayılarına dayalı karma kodları atamak istediğimizi düşünün. Bu durumda, öğrenci numarasının ilk iki basamağını mı yoksa son iki basamağını kullanmak daha mantıklı olur? Cevabınızı doğrulayın. Alıştırma 5.2. Bölüm 5.1.1 'de verilen karma şemayı düşünün ve varsayalım ki, n=2d ve d ≤ w/2. 2. Seçilen herhangi bir z çarpanı için hepsi aynı hash koduna ait n değer mevcut olduğunu gösterin. (İpucu: Bu kolaydır ve herhangi bir sayı teorisi gerektirmez.) 2. z çarpanı verildiğinde, hepsi aynı hash koduna ait n değeri açıklayın. (İpucu: Bu zordur ve birtakım temel sayı teorisi gerektirir.) Alıştırma 5.3. x = 2w–d–2 ve y = 3x ise Pr{hash (x)} = Pr{hash (y)} = 2/2d olduğunu göstererek, Önerme 5.1 ile verilen 2/2d sınırının mümkün olan en iyi sınır olduğunu kanıtlayın. (İpucu: İkili tabanda zx ve z3x gösterimlerine bakın ve z3x = zx+2zx gerçeğini kullanın.) Alıştırma 5.4. Bölüm 1.3.2 'de verilen Stirling'in Yaklaşımı’nın tüm yorumunu kullanarak Lemma 5.4 tekrar kanıtlayın. Alıştırma 5.5. Bir Doğrusal Karma Tablo’ya x elemanını eklemek için ilk null dizi girdisinde x elemanını depolayan aşağıdaki kodun aşağıdaki basitleştirilmiş versiyonunu düşünün. O(n) zamanda çalışan bir dizi ekle(x), sil(x) ve bul(x) işlemlerini örnek vererek bunun n2 zamanda neden çok yavaş çalışabileceğini açıklayın. 137 Alıştırma 5.6. String sınıfı için Java hashCode() yönteminin önceki sürümleri uzun dizelerde bulunan tüm karakterleri kullanmayarak çalışıyordu. Örneğin, on altı uzunluğunda karakter dizesi için, karma kodu yalnızca sekiz çift endeksli karakterler kullanılarak hesaplanmıştır. Hepsi aynı karma koduna sahip büyük miktarda dizeleri örnek göstererek bunun neden çok kötü bir fikir olduğunu açıklayın. Alıştırma 5.7. Varsayalım ki, iki w-bit tamsayı olan x ve y bir nesne oluştursun. Neden x ⊕ y fonksiyonu böyle bir nesne için iyi bir karma kodu sağlamaz, gösterin. Tamamen karma kodu 0’lardan oluşan nesneler içeren büyük bir küme örneği verin. Alıştırma 5.8. Varsayalım ki, iki w-bit tamsayı olan x ve y bir nesne oluştursun. Neden x + y fonksiyonu böyle bir nesne için iyi bir karma kodu sağlamaz, gösterin. Tamamen karma kodu birbirinin aynı olan nesneler içeren büyük bir küme örneği verin. Alıştırma 5.9. Varsayalım ki, iki w-bit tamsayı olan x ve y bir nesne oluştursun. Nesne için karma kodu sadece bir w-bit tamsayı üreten h(x, y) deterministik fonksiyonu ile tanımlansın. Aynı karma koduna sahip olan nesnelerin büyük bir küme oluşturduğunu kanıtlayın. Alıştırma 5.10. Bazı pozitif tamsayı w için p=2w-1 olsun. Neden bir pozitif tamsayı x için olduğunu açıklayın. (Bu x ≤ 2w - 1 olana dek ard arda 138 belirlemesiyle x mod (2w – 1) hesabını gerçekleştirmek için bir algoritma verir.) Alıştırma 5.11. Sık kullanılan bazı karma tablo uygulamalarını bulun (Java Collection Framework, HashMap ya da bu kitapta yer alan Karma Tablo veya Doğrusal Karma Tablo uygulamaları gibi) ve bu veri yapısındaki x tamsayıları için bul (x) işleminin doğrusal zaman aldığı tamsayıları depolayan bir program tasarlayın. Yani, aynı tablo konumuna karma edilen cn elemandan oluşan bir n tamsayıdan oluşan bir küme bulun. Uygulamanın ne kadar iyi olduğuna bağlı olarak, sadece uygulama kodunu inceleyerek bunu yapmak mümkün olabilir, ya da belirli değerleri eklemenin ve bulmanın ne kadar zaman aldığını belirlemek için deneme ekleme ve arama işlemlerini gerçekleştiren bazı kodları yazmak zorunda kalabilirsiniz (Bu web sunucularına [17] tehdit niteliği taşıyan denial-ofservice12 saldırılarını başlatmak için kullanılabilir, ve kullanılmıştır.) 12 Hizmet reddi anlamına gelir. 139 Bölüm 6 İkili Ağaçlar Bu bölüm bilgisayar bilimlerinin en temel yapılarından birini tanıtıyor: İki Dala Ayrışmış Ağaç13. Burada köken olarak ağaç sözcüğünün kullanılması kağıda çizdiğimizde, sık sık bir ormanda bulunan ağaca benzer şekilleri andırmasından ileri gelmektedir. İkili ağacı tanımlamanın birçok yolu vardır. Matematiksel olarak, ikili ağaç bağlı ve yönsüz hiçbir döngüsü olmayan sonlu grafiktir ve üç dereceden daha büyük dereceye sahip olmayan tepe noktasına sahiptir. Çoğu bilgisayar bilimi uygulamaları için, ikili ağaçlar köklüdür: En çok ikinci derecedeki özel bir r düğümü ağacın kökü olarak adlandırılır. Her u ≠ r düğümü için u’dan r’ye yol üzerindeki ikinci düğüm u’nun menşei olarak adlandırılır. u’ya bitişik diğer düğümlerin her biri, çocuğudur. İlgileneceğimiz ikili ağaçların çoğu sıralıdır, bu yüzden sol ve sağ çocukları ayrı düşünmenizde fayda vardır. Gösterimlerde, ikili ağaçlar genellikle aşağı doğru kökten yönlenir, çizimin üst kısmında kök vardır. Sol ve sağ çocuklar sırasıyla şekilde (Şekil 6.1) gösterildiği gibi sol ve sağ dallara ayrılır. Örneğin, Şekil 6.2. dokuz düğümlü ikili ağaç gösteriyor. Önemli bir yeri var olan ikili ağaç kavramında birçok yeni terminolojiye duyulmuştur. Geniş kabul gören bazı tanımları listelemek gerekirse, derinlik, ata, çocuk, yükseklik, yaprak, altağaç sayılabilir. İkili ağaç içinde bir u düğümünün derinliği, u düğümü ile kök arasındaki yolun uzunluğudur. Bir u düğümünde durduğunuzu farz edin. w ile belirtilen bir düğüm sizinle başka bir r arasında kalıyorsa w sizin atanız olur, ve siz w ile belirtilen düğümün çocuğu sayılırsınız. Üzerinde durduğunuz düğümden bir altağaç üremişse, siz o altağacın kökü sayılırsınız. Bu altağaç sizin tüm çocuklarınızı içerir. Size ait olan bir düğümün yüksekliği, çocuğunuza giden yolun en uzun mesafesidir. Bir ağacın yüksekliği siz kökte iken hesaplanan yüksekliktir. Hiçbir çocuğunuz yoksa düğümünüz yapraktır. 13 Bu terimi İkili Ağaç olarak tanımlamak çevirmenler tarafından önerilen makul ifadedir. 140 Bazı durumlarda, ikili ağacın uç düğümlerini dış düğüm olarak değerlendirmek gereklidir. Sol çocuğu olmayan düğümlerin soluna bir dış düğüm, sağ çocuğu olmayan düğümlerin sağına bir dış düğüm yerleştirmek (Şekil 6.2.b) ikili ağacın kolay doğrulanabilir özelliklerini anlamamıza yardımcı olur. Örnek vermek gerekirse, n ≥ 1 koşulunu sağlayan n gerçek düğümlü bir ikili ağacın n+1 dış düğümü vardır. 141 6.1 İki Dallı Ağaç: Temel İkili Ağaç u düğümünün ikili ağaç olduğunu ifade etmenin en temel kriteri en çok üç düğümle bağlantılı olduğunu sağlamasıdır. Bu üç düğüm komşu bağlantılar olarak tanımlanır: Ağaçlık koşulunun devamlılığını sağlamak niyetiyle bu üç komşudan biri mevcut değilse, değeri nil olarak atanır. Bununla beraber, ağacın dış düğümleri ve kökün menşei de nil olacaktır. İkili ağacın kendisi köküyle ifade edilmektedir. Kök r ise : Bir düğümün u’dan köke yol uzunluğu ile ölçülen derinliği, u, hesaplanabilir bir niceliktir : 142 6.1.1 Özyinelemeli Algoritmalar Özyinelemeli algoritmalar kullanılarak ikili ağaç hakkında kolay hesaplanır gerçekler ifade edilir. Örneğin kökü, u, olan bir ikili ağaç için düğüm sayısını (büyüklüğünü) hesaplamak için özyinelemeli olarak u düğümünde köklenmiş iki altağacın boyutları hesaplanır, sonuç bu ikisinin toplamının bir fazlası olarak dönebilir: u düğümünün yüksekliğini hesaplamak için, u’nun iki altağacının yüksekliği hesaplanır, en büyüğünün bir fazlası dönebilir: 6.1.2 İkili Ağaçta Sıralı-Düğüm Ziyaretleri Bir önceki bölümde elde edilen iki algoritmanın her ikisi de, ikili ağaçtaki tüm düğümleri ziyaret için özyineleme tekniğini kullanır. İkili ağacın düğümlerini ziyaret etme sırasını belirleyen yazılan kodun kendisidir: 143 Özyinelemenin ikili ağaca bu şekilde uyarlanması kodu basitleştirir, ancak sorun da yaratması olasıdır. Özyinelemenin maksimum derinliği ikili ağaçta bir düğümün maksimum derinliği, yani ağacın derinliği ile verilir. Ağacın yüksekliği çok fazla olduğu takdirde, özyineleme kendisini fazla tekrar edeceği için Yığıt için ayrılan bellek sömürülerek kullanımı olanak dışı olur ve çöküşe sebep olur. Özyineleme olmadan bir ikili ağaç hesaplaması için nereden geldiğine göre nereye gideceğini kendisi belirleyen bir algoritma kullanabilirsiniz. Bkz. Şekil 6.3. u.parent bir u düğümüne ulaşırsa, daha sonra yapılacak bir sonraki şey sol altağacı, u.left, ziyaret etmek olacaktır. Bu yönden u düğümüne yapılacak bir ziyaret bize sağ altağacın, u.right, yollarını açacaktır. Bu yönden u düğümüne yapılacak bir ziyaret ile u altağacı tamamıyla ziyaret edilmiş olur. Altağaç ziyareti tamamlandıktan sonra menşei düğüm ziyareti ile birlikte süreç sona erer. Aşağıdaki kod, sol, sağ ve menşei düğümlerin nil olmasına izin verecek şekilde bu fikri uyguluyor: Özyinelemeli algoritmalar ile denenen hesaplamalar özyinelemesiz kod için geçerlidir. Örneğin, ağaç boyutunu hesaplamak için, her düğüm ziyaretinde bir sayı değişken artırılarak çözüm bulunabilirdi: 144 İkili ağaçların bazı uygulamalarında, menşei düğüm kullanılmaz. Bu durumda, özyinelemeli olmayan bir uygulama hala mümkündür, ancak bulunduğumuz düğümden köke giden yolda Liste (yada Yığıt) veri yapılarından herhangi birinin kullanılması uygun ve gerekli olacaktır. 145 Yukarıdaki fonksiyonlara ait gözlemler göstermiştir ki belli özel desen kalıpları ziyarete göre değişmektedir. Bunun anlamı önce ziyaretin deseni belirlenmelidir. Enine aramada düğümler düzey-düzey ziyaret edilmektedir. Kökten başlar, aşağı doğru ilerler, soldan sağa doğru her seviyede düğümler ziyaret edilir (Bkz. Şekil 6.4) Okuyucu katılmayabilir, ancak bazı insanlar, buna okumak diyor. Bu desen Kuyruk veri yapısı ile temsil edilir. Kuyruk’ta kök, r, her şeyiyle (veri) hizmete yakın pozisyonda bulunmalıdır. Her adımda, bir sonraki u düğümünü q düğümünden çıkarmalı, sonrasında u işleme konmalı, ve nil değilse sol, sağ düğümler Kuyruk’a eklenmelidir. Bu ziyaret istenen deseni size veren bir sırayla düğümleri ziyaret eder. 6.2 Serbest Yükseklikli İkili Arama Ağacı Serbest Yükseklikli İkili Arama Ağacı u.data verisini belirli kriterlere göre taşıyan ve beklendiği üzere u düğümlerini depolayan özelleştirilmiş bir ikili arama ağaç türüdür. Serbest Yükseklikli İkili Arama Ağacı’nın özellikleri, örneğin, büyüklük sırası soldan sağa doğru sıralanacaksa, 1) u.left < u.x (yani, sol altağaç elimizdeki değerden küçüktür) yada 2) u.right > u.x (yani, sağ altağaç elimizdeki değerden büyüktür) şartları gözetilir. Bu şartı koşulsuz her düğüm için sağlayan yapı Serbest Yükseklikli İkili Arama Ağacı ’dır. (Bkz. Şekil 6.5.) 146 6.2.1 Arama İkili arama ağacı yapısında bir değere ulaşmak oldukça kolaydır. Kökten başlanır, herhangi bir x düğümü için üç durum söz konusudur: 1. u.left takibi için x < u.x koşulu aranır. 2. u.right takibi için x > u.x koşulu aranır. 3. x = u.x ise aranan düğüme ulaşılmıştır. 3.durum oluştuğunda veya u=nil olduğu takdirde arama birincisinde başarılı, ikincisinde başarısız sonuç döndürür. Serbest Yükseklikli İkili Arama Ağacı ’nda gerçekleştirilebilecek aramalara iki örnek Şekil 6.6’da gösterilmiştir. İkinci örnekte gösterildiği gibi aramanın başarısız olduğu durumlar hakkında yargıda bulunalım: 1.durum oluştuğunda en son u düğümüne bakarsanız, u.x için büyüklük karşılaştırması yapmaya gerek duymadan x’den büyük olan en küçük değer yargısı verebiliriz. Serbest Yükseklikli İkili Arama Ağacı bize bunu garanti eder. 2.durum oluştuğunda, benzer şekilde, x’den küçük olan en büyük değer yargısı verilir. Bu matematiksel yargıların yararlı özellikleri Serbest Yükseklikli İkili Arama Ağacı ’nın içine gömülmüştür. Ancak, arama sonucu onlar belirgin hale gelir. Bu bize arama sonucunu bulmak için tasarlanan bir bul(x) işleminde kullanılabilecek karşılaştırmaların x hedefine sizi götürmede ne kadar kuvvetli bir araç olduğunu size gösteriyor. 147 6.2.2 Ekleme Serbest Yükseklikli İkili Arama Ağacı ’na yeni bir değer eklemek için, ilk olarak x aranır. Onu bulursanız, eklemeye izin verilmez. Aksi takdirde, x için yapılan aramanın en sonunda ulaşılan p düğümünün x ve p.x karşılaştırması yapılır. Büyüklük sıralaması ne gerektiriyorsa sonuca göre elimizdeki p düğümünün sol yada sağ yaprağına x eklemesi doğru bir şekilde gerçekleştirilmiş olur. 148 Bir örnek, Şekil 6.7’de gösterilmiştir. Bu sürecin en zaman-alıcı parçası yeni eklenen, u, düğümünün yüksekliğine orantıyla bağlı zamanda çalışan ve ilk yapılan x aramasıdır. En kötü durumda, bu Serbest Yükseklikli İkili Arama Ağacı ’nın yüksekliğine eşittir. 149 6.2.3 Silme Ağacın bir, u, düğümünü (verisiyle birlikte) silme bir İkili Arama Ağacı’nda biraz daha zordur. Silinecek u düğümü bir yaprak ise sadece menşei’nden onu ayırmak yeterli olacaktır. Bunun için daha iyi bir yöntem vardır: u’nun sadece bir çocuğu varsa, ağaçtan u çıkarılarak u’ya bağlı düğümler birbirine uç uça eklenir. u menşei, kendi çocuğunu evlat edinmiştir (Bkz. Şekil 6.8): 150 Silinecek düğümün ağaçta halihazırda iki çocuğu varsa, ikiden az çocuk için uygun bir arama yapılmalıdır. Uygunluk kriteri şöyle seçilmelidir: Her ne olursa olsun Serbest Yükseklikli İkili Arama Ağacı özellikleri korunmalıdır. Silinecek düğümün değerini seçmek için u düğümünün sağ altağacında u.x değerinin büyük sıralamasına göre bir sonra gelen büyüğü aranır. Her düğüm için sağ çocuk değeri sol çocuktan büyüktür. O zaman sağ çocuğa ait altağacın yapraklarından küçüğünü u’nun değeri olarak atayabiliriz. Bu bize gerekli büyüklük sıralamasını sağlar. Böyle bir w düğümü bulunduğunda, u.x değeri w’ye aktarılır. Dikkat edilmesi gereken bir konu da u civarında w düğümleri aranmalıdır. w.x değeri u.x değerine yakın olmalıdır. Bunun için u.right altağacında u.x’den büyük en küçük değeri bulunur. Bu değeri çocuğu olmadığı için silmek kolaydır (Bkz. Şekil 6.9). 151 6.2.4 Özet Serbest Yükseklikli İkili Arama Ağacı ’nda gerçekleştirilen bul(x), ekle(x) ve sil(x) işlemleri her ağacın kökünden bir yol izlemeyi gerektiriyor. Yol uzunluğu düğümlerin lokasyonuna bağlıdır. Aşağıdaki teorem Serbest Yükseklikli İkili Arama Ağacı veri yapısının performansını özetler: Teorem 6.1. Serbest Yükseklikli İkili Arama Ağacı, Sıralı Küme arayüzünü uygular. İşlem başına O(n) beklenen zamanda çalışan ekle(x), sil(x) ve bul(x) işlemlerini destekler. Serbest Yükseklikli İkili Arama Ağacı yapısı ile ilgili sorun yüksekliğinin girdi verilerin geliş sırasına bağlı olarak önceden belirleyememiş olmamızdır. Yani sadece işlemler değil, ağaç yüksekliği de O(n) olabilir, işlem sayısı çok ise performans olumsuz etkilenir. Verilerin geliş 152 sırasına bağlı olarak, eğer bir ikili ağaç zincir oluşmuşsa (yani, son düğüm dışında her düğümün yalnız bir çocuğu varsa), Teorem 6.1 yeterli olmaz. Teorem 4.1 bir çözüm olabilir, çünkü Sıralı Küme Sekme Listesi yapısının gerçekleştirimi Sıralı Küme arabirimi ile O(log n) zamanda çalışır. Serbest Yükseklikli İkili Arama Ağaç’larını kullanmak istemiyorsanız O(log n) zamanda çalışan işlemleri gerçekleştiren veri yapıları arasında, Bölüm 7’de işlemlerin beklenen zamanlarının O(log n) sınırında nasıl rasgele elde edildiğini, Bölüm 8’de amortize çalışma zamanlarını O(log n) zamanda tutacak yerel ve tekrarlı oluşturma tekniğini, Bölüm 9’da enkötü-durum çalışma zamanlarının en fazla 4-çocuğa kadar düğümleri kabul eden, yani ikiliolmayan, bir ağaç yapısı ile nasıl gerçekleştirildiğini bulabilirsiniz. 6.3 Tartışma ve Alıştırmalar İkili ağaçlar binlerce yıldır ilişki modellemek için kullanılmıştır. Bunun bir nedeni, ikili ağaçlar soy ağaçlarının doğal modeli olabilir mi sorusuydu. Aile ağacı demek daha doğrusudur. Kök bir insan, sol ve sağ çocuklar ana-baba, ve özyinelemeli olarak devamı sayılabilir. Daha yakın yüzyıllarda, ikili ağaçlar biyolojide tür kavramını modellemekte kullanıldı. Ağacın yaprakları kaybolmamış türleri temsil ederken, iç düğümleri türleşmeyi temsil eder. Örnek olarak, tek bir türün iki ayrı türe dönüşmesine neden olan olayların iki ayrı popülasyonu nasıl oluşturduğu problemi verilebilir. İkili arama ağaçlarının 1950'lerde çeşitli gruplar tarafından bağımsız olarak keşfedildiği düşünülmektedir [48, Bölüm 6.2.2]. İkili arama ağaçlarının koşulları değiştirilerek uygulanan tasarımları sonraki bölümlerde anlatılıyor. Sıfırdan bir ikili ağaç uygularken birkaç tasarım noktasına karar verilmesi gerekir. Bunlardan ilk soru her düğüm menşei’ne işaretçi atamış mıdır? Kökten başlayıp yaprağa giden yol izlerseniz, menşei işaretçilerini depolamak zaman ve bellek kaybıdır, gerek yoktur. Ziyaret performansı açısından düşünürseniz, menşei kayıtları savunulur. Özyinelemeli olarak veya dışsal Yığıt yoluyla gerçekleştirmek mümkün olur. Diğer bazı yöntemler için de (eş-dağılımlı yükseklikli ikili arama ağaçlarındaki ekleme veya silme gibi) menşei kayıtları tavsiye edilir. 153 Başka bir tasarım sorusu her düğüm için ana-baba’yı nasıl temsil etmek gerekir? Bu bölümdeki gerçekleştirim ayrı değişkenler olarak ana-baba’yı depoladı. Başka bir seçenek uzunluğu 3 olan bir dizi, p ana-baba’yı saklar. u.p[0] sol çocuğu, u.p[1] sağ çocuğu, u.p[2] menşei temsil eder. Bir dizi içinde bu temsil, if gibi, birtakım işlem kontrol cümlelerinin cebir açılımlarına olanak tanır. Ziyaretlerin açıklanması böyle bir sadeleşmeye dayanak sağlar. u.p[i] yönünden gelen bir ziyaretin bir sonraki adımı (u.p[i+1] mod 3) yönünden gelir. Sağ-sol simetrisi olduğunda benzer örnekler ortaya çıkar. u.p[i] için ağaçta kardeş görünüyorsa bu muhakkak ki (u.p[i+1] mod 2) düğümüdür sağaÇevir(u) (ikili ağaç olduğu için). Sayfa 163’te belirtilen solaÇevir(u) , kodları size cebirsel açıklamanın kodu anlamanız için temel sağlayan bir araç olduğunu gösteriyor. Alıştırma 6.1. n ≥ 1 düğüme sahip ikili ağacın kenar sayısı n – 1 midir? Kanıtlayın. Alıştırma 6.2. n ≥ 1 gerçek düğüme sahip ikili ağacın dış sayısı n + 1 midir? Kanıtlayın. Alıştırma 6.3. İkili, T, ağacın en az bir yaprağı varsa, (a) T 'nin kökü en az bir çocukludur. (b) T birden fazla yapraklıdır. Kanıtlayın. Alıştırma 6.4. u düğümünden köklü bir altağacın boyutunu hesaplayan boyut2(u) adında özyinelemeli-olmayan yöntemi uygulayın. Alıştırma 6.5. İkili Arama Ağacı’ndaki bir u düğümünün yüksekliğini hesaplayan yükseklik2(u) adında özyinelemeli-olmayan yöntemi uygulayın. Alıştırma 6.6. Eş-dağılımlı ikili ağacın boyutu dengeli olduğu için (Tanım: her u düğümü için sol ve sağ altağaçların düğüm sayısı (eleman) en fazla bir farklılık gösterirse ağaca boyutdengeli denir.) boyutDengeli() adında özyinelemeli bir yöntem uygulayın. Boyut dengeli ise true döndürsün. O(n) çalışma zamanı olsun. Düğümleri farklı lokasyonlarda olan büyük ağaçlarda kodu sınayın. O(n) sağlamayan daha çok zaman alan kod yazmak nispeten kolay olacaktır. 154 İkili ağaçta öncül-sıra ziyareti her u düğümünü çocuğundan önce ziyaret eder. içsel-sıra ziyareti her u düğümünü sol altağacını, hemen ardından sağ altağacını, ziyaret ettikten sonra ziyaret eder. arkalı-sıra ziyareti her u düğümünü altağacını ziyaret ettikten sonra ziyaret eder. Ağacın düğüm lokasyonu öncül/içsel/arkalı-sırada ise elemanlarının öncül/içsel/arkalıziyarete göre sıralanması olacaktır. 0, …., n – 1 için bir örnek Şekil 6.10’da verilmiştir. Alıştırma 6.7. Arama Ağacı’nın bir alt sınıfını oluşturun. Bu alt sınıfın veri alanları öncülsıra, arkalı-sıra, ve içsel-sıra içeren sayıları depolamakta kullanılsın. öncülSıradaSayılar(), içselSıradaSayılar() ve arkalıSıradaSayılar() adlarında yöntemler geliştirin. Bu yöntemler ağaçta depolanan sayıları belirtilen sırada yazsınlar. Her birinin çalışma zamanı O(n) olmalıdır. Alıştırma 6.8. u’dan sonra belirtilen sırada gelmesi gerekli düğümü getiren fonksiyonlar amortize sabit zamanda çalışabilir. Herhangi bir u düğümünden başlatıldığında ve tekrar tekrar bu fonksiyonlardan birini çağırırsanız, u=null olana kadar dönüş değerini u’ya atadığınızı düşünün. Bu işlemlerin maliyeti O(n) midir? sonrakiÖncülSıra(u), sonrakiİçselSıra(u), ve sonrakiArkalıSıra(u) fonksiyonlarını özyinelemeli olmayan şekilde uygulayın. Alıştırma 6.9. Varsayın ki, düğümlerdeki lokasyonları öncül-, arkalı- ve içsel-sırada olan sayıları depolayan ikili ağaç verilsin. Şu soruları cevaplarının “-sabit zaman” olması için bu rakamları kullanabileceğinizi gösterin: 1. u düğümünden başlayan altağacın boyutunu belirleyin. 1. u düğümünün derinliğini belirleyin. 3. u ve w düğümlerinden biri diğerinin atası mıdır? Belirleyin. (u, w düğümleri aynı değildir.) Alıştırma 6.10. öncül-sıra ve içsel-sıra taşıyan düğümlerin listesi en çok bir ağaçta toplanabilir. Kanıtlayın ve ağacı oluşturun. Düğüm listesinin verildiğini varsayabilirsiniz. Alıştırma 6.11. Herhangi bir ikili ağaçtaki düğümlerin lokasyonu en çok 2(n – 1) bit kullanılarak temsil edilebilir gösterin. (İpucu: Bir ziyaret sırasında ne olabileceğini düşünün ve ağacı oluşturmak için bu olacakları zihninizde canlandırın.) 155 Alıştırma 6.12. Şekil 6.5’te gösterilen ikili arama ağacına 3,5 ve 4,5 değerlerini eklediğinizde olacakları tanımlayın. Alıştırma 6.13. Şekil 6.5’te gösterilen ikili arama ağacından 3 ve 4 değerlerini sildiğinizde olacakları tanımlayın. Alıştırma 6.14. küçüklerinToplamListesi(x) adında x elemanından küçük düğümlerin değerlerini hesaplayan bir listeyi bize veren İkili Arama Ağacı yöntemini uygulayın. Yönteminiz O(n’ + h) zamanda çalışmalı x elemanından küçük eleman sayısı n’ ile belirtilsin ve h ağacın yüksekliğini belirtsin. Alıştırma 6.15. Başlangıçta boş olan ancak yazdığınız kod sayesinde n – 1 yüksekliğine erişen boş İkili Arama Ağacı’na elemanları nasıl ekleyebilirsiniz açıklayın. Kaç şekilde bunu yapmanız mümkün olur? Alıştırma 6.16. İkili Arama Ağacı ve ekle(x), hemen ardından sil(x) gerçekleştirirseniz (x aynı değeri taşıyor olacak) arama ağacındaki düğümlerin lokasyonu mutlaka değişir mi? Farklı kalabilir mi? Aynı olmak zorunda mıdır? Alıştırma 6.17. İkili Arama Ağacı’nda sil(x) işlemi, herhangi bir düğümün yüksekliğini arttırabilir mi? Evetse, kaç yüksekliğinde artırır? Alıştırma 6.18. İkili Arama Ağacı’nda ekle(x) işlemi, herhangi bir düğümün yüksekliğini arttırabilir mi? Ağacın yüksekliğini artırabilir mi? Evetse, kaç yüksekliğinde artırır? Alıştırma 6.19. İkili Arama Ağacı’nın bir versiyonunu tasarlayın ve uygulayın. u düğümünden başlayan altağacın boyutu u.boyut ile belirlensin. Derinliği u.derinlik ile belirlensin. Yüksekliği u.yükseklik ile belirlensin. belirle(x) ve sil(x) işlemleri bu değerleri değiştirebilir, ancak bu maliyetini artırabilir olmamalıdır, sadece sabit oranda artırır veya azaltır şartı aranıyor. 156 Bölüm 7 Rasgele İkili Arama Ağaçları Bu bölümde, tüm işlemler için beklenen O(log n) çalışma zamanını rasgelelik kullanarak sağlayan İkili Arama Ağacı tanıtılıyor. 7.1 Rasgele İkili Arama Ağaçları Şekil 7.1’de gösterilen iki ikili arama ağaçları düşünün, bunların her birinin n = 15 düğümü vardır. Soldaki bir listedir, ve öteki mükemmel dengeli ikili arama ağacıdır. Soldaki n – 1 = 14 yüksekliğe sahiptir, sağdakinin yüksekliği üçtür. Bu iki ağacın nasıl kurulduğunu düşünelim. Soldaki, boş İkili Arama Ağacı ile başlandığında ve aşağıdaki veri dizisi ağaca sırasıyla eklendiğinde elde edilir. Başka sırada yapılan hiçbir ekleme bu ağacı oluşturmaz (n üzerinde tümevarım yöntemiyle kanıtlanacağı üzere). Diğer taraftan sağdaki ağaç, aşağıdaki dizi tarafından oluşturulabilir: Aşağıdaki diğer diziler de bu ağacı oluşturmak için dahil edilebilir: ve 157 İşin gerçeği, sağdaki ağacı oluşturan 21.964.800 dizi vardır ve sadece bir dizi soldaki ağacı oluşturur. Yukarıdaki örnek bazı sözel ifade edilen kanıtları bize sağlamaktadır: 0 … 14 arasında rasgele permütasyon seçilerek ikili arama ağacına eklenirse, o zaman çok dengeli bir ağaç elde etmek (Şekil 7.1’in sol tarafı) çok dengesiz bir ağaç elde etmekten daha olasıdır (Şekil 7.1’in sağ tarafı) denebilir. Bu kavramı rasgele ikili arama ağaçlarını inceleyerek bilimsel bir şekilciliğe şöyle büründürebiliriz: n boyutunda bir Rasgele İkili Arama Ağacı’nı şu şekilde elde edebiliriz: 0, …, n - 1 arasında rasgele x0, …, xn-1 permütasyonu seçin ve elemanlarını teker teker İkili Arama Ağacı’na ekleyin . Rasgele permütasyonun anlamı şudur: 0, …, n – 1 sayılarının olası her bir n! permütasyonu (sıralaması) eşit derecede olasıdır, yani herhangi bir permütasyonu elde etme olasılığı 1/n! olur. Unutmayın ki, 0, …, n – 1 değerleri, Rasgele İkili Arama Ağacı’nın özelliklerinden herhangi birini değiştirmeden, herhangi bir sıralı n eleman dizisi ile yer değiştirebilir.x ∈ {0, …, n – 1} elemanı n boyutunda sıralı bir küme içinde x’nci sıradaki elemanı belirtiyor. Rasgele İkili Arama Ağaç’ları hakkında verilen ana sonuca gelmeden önce, konudan biraz uzaklaşarak randomize yapılar incelenirken sık sık gündeme gelen bir sayı türünü gündeme getireceğiz. Negatif olmayan bir k tamsayısı için, Hk ile gösterilen k’nci harmonik sayı, 158 olarak tanımlanır. Harmonik sayı Hk hiçbir basit kapalı formu yoktur, ama çok yakından k’nın doğal logaritması ile çok yakından ilgilidir. Özellikle, Matematik okumuş olan okuyucular fark edebilirler ki, olması nedeniyle bu böyledir. İntegral’in bir eğri ve x-ekseni arasındaki alan olarak yorumlanabildiğini akılda tutarak, Hk değeri integrali tarafından alt-sınırlanmış ve tarafından üst-sınırlanmış olabilir (Grafiksel açıklama için bkz. Şekil 7.2). Önerme 7.1. n boyutunda Rasgele İkili Arama Ağacı’nda herhangi bir x elemanı için arama yolunun beklenen uzunluğu şöyle ifade edilir: 1. Herhangi bir x ∈ {0, …, n – 1} için olur.14 2. Herhangi bir x ∈ (–1, n) \ {0, …, n – 1} için 14 olur. x + 1 ve n – x ifadeleri sırasıyla ağaçta yer alan x değerinden az veya eşit olan eleman sayısı ve x değerinden büyük veya eşit olan eleman sayısı olarak yorumlanabilir. 159 Sonraki bölüm Önerme 7.1’i kanıtlıyor. Şimdilik, Önerme 7.1’in iki bölümünün bize ne düşündürdüğü ile ilgilenelim. Birinci bölüm, n boyutunda bir ağacın içindeki bir eleman için arama yaparsanız, arama yolunun beklenen en fazla uzunluğunun 2ln n + O(1) olduğunu bize anlatıyor. İkinci bölüm bize ağaçta depolanmayan bir değer için yapılan arama hakkında aynı şeyi anlatır. Önermenin bu iki bölümü karşılaştırıldığında ağaçta yer alan bir elemanı aramanın, ağaçta yer almayan bir elemanı aramaktan sadece biraz hızlı olduğu görülüyor. 7.1.1 Önerme 7.1’in Kanıtı Önerme 7.1’i kanıtlamak için ihtiyaç duyulan en önemli gözlem şudur: Ancak ve ancak T’yi oluşturmak için kullanılan rasgele permütasyonda elemanlarından önce gelen bir, i < x düğümü (–1, n) açık aralığındaki bir x değeri için yapılacak T, Rasgele İkili Arama Ağacı’ndaki arama yolu içinde bulunur. Bunu görmek için bkz. Şekil 7.3’te fark etmelisiniz ki eklenene kadar, elemanlarından biri açık aralığındaki her değer için arama yolları özdeştir. (Unutmayın ki, iki değerin farklı arama yolları olması için, ağaçta onlarla farklı karşılaştırılan bazı elemanların bulunması gerekir.) içinde rasgele permütasyonda görünür ilk eleman j olsun. Dikkat edin, x için yapılan her arama yolunda j yol üzerinde bulunmak, yani görünmek zorundadır. Elemanların farklı olduğu durumda, yani j ≠ i ise, j elemanını içeren uj düğümü i elemanını içeren ui düğümünden önce oluşturulmuştur kesin olarak doğrudur, çünkü i < j (soldan sağa doğru yer alırlar.) Bu nedenle, şu da kesin olarak doğrudur ki, ağaca i elemanı eklendiğinde, uj.left kökenli altağaca eklenir. Diğer taraftan, x için arama yolu bu altağacı hiçbir zaman ziyaret etmeyecektir, çünkü uj düğümü ziyaret edildikten sonra uj.right altağacına doğru ziyarete devam edilecektir. Benzer şekilde ancak ve ancak T’yi oluşturmak için kullanılan rasgele permütasyonda elemanlarından önce gelen i > x düğümü, T, Rasgele İkili Arama Ağacı’ndaki arama yolu içinde bulunur. 160 {0, …, n} rasgele permütasyonu verildiği takdirde, sadece ve dizileri kendi kendilerinin rasgele permütasyonları olur. ve altkümelerinin her elemanı eşit olasılıkla, T’yi oluşturmak için kullanılan rasgele permütasyondaki herhangi bir alt kümenin herhangi bir elemanından önce görünecektir. Bu yüzden, Bu gözlem ile, Önerme 7.1’in kanıtı harmonik sayılar ile bazı basit hesaplamalar içermektedir: 161 Önerme 7.1’in Kanıtı: Ii göstergesi x için yapılan arama yolunda i bulunduğunda bir, ve aksi takdirde sıfır değerine sahip rasgele değişken olsun. Arama yolunun uzunluğu olarak ifade edilir. Bu nedenle, x ∈ {0, …, n – 1} için arama yolunun beklenen uzunluğu (bkz. Şekil 7.4.a) olur. 162 x ∈ (–1, n) \ {0, …, n – 1} arama değerleri için ilgili hesaplamalar hemen hemen aynıdır (Bkz. Şekil 7.4.b). 7.1.2 Özet Aşağıdaki teorem Rasgele İkili Arama Ağacı’nın performansını özetler: Teorem 7.1: Rasgele İkili Arama Ağaç, O(n logn) zamanda oluşturulabilir. Rasgele İkili Arama Ağacı’nda, bul(x) işlemi, O(log n) beklenen zamanda çalışır. Tekrar vurgulamak gerekir ki, Teorem 7.1’deki beklenti, Rasgele İkili Arama Ağacı’nı oluşturmak için kullanılan rasgele permütasyona uygulanmıştır, rasgele x seçimlerine bağlı değil, her x değeri için doğru kabul etmelisiniz. 7.2 Treap: Randomize İkili Arama Ağaçları Rasgele İkili Arama Ağacı ile ilgili sorun dinamik olmamasıdır. Sıralı Küme arayüzünü uygulamak için gerekli ekle(x) veya sil(x) işlemlerini desteklemez. Bu bölümde Treap adında ve Sıralı Küme arayüzünü uygulamak için Önerme 7.1’i kullanan bir veri yapısını tanımlıyoruz15. Treap düğümü, x veri değerine sahip olması ve aynı zamanda p adı verilen ve rasgele atanan özel bir sayısal öncelik içermesiyle, İkili Arama Ağacı düğümüne benzer özellikler taşımaktadır: 15 Treap veri yapısının adı, aynı anda bir ikili arama ağacı olmasından gelen Tree (Bölüm 6.2) ve Heap (Yığın, Bölüm 10) sözcüklerini oluşturan eklerin birleşiminden almıştır. 163 İkili Arama Ağacı • olmasının yanı sıra, Treap düğümleri Yığın özelliklerini de sağlar: (Yığın Özelliği) Kök hariç, her u düğümünde, u.parent.p < u.p Diğer bir deyişle, her düğüm iki çocuktan daha küçük bir önceliğe sahiptir. Bir örnek, Şekil 7.5’te gösterilmiştir. Her düğüm için key (x) ve priority (p) tanımlanmasının gerçekleşmesiyle birlikte, Yığın ve İkili Arama Ağacı koşulları Treap şeklini tam olarak belirler. Yığın özelliği, minimum öncelikli, r, düğümünün kök olması gerektiğini sağlar. İkili Arama Ağacı özelliği r.x’den küçük anahtara sahip tüm düğümlerin r.left kökenli altağaçta saklanmasını, ve r.x’den büyük anahtara sahip tüm düğümlerin r.right kökenli altağaçta saklanmasını sağlar. Bir Treap’te öncelik değerleri hakkında önemli olan nokta onların tekil ve rasgele atanmalarıdır. Bu nedenle, Treap düşüncesi için iki eşdeğer yol vardır: Yukarıda tanımlandığı gibi Treap, Yığın ve İkili Arama Ağacı özelliklerini sağlar. Alternatif olarak, Treap düğümlerini artan öncelik sırasıyla eklenen bir İkili Arama Ağacı olarak düşünebilirsiniz. Örneğin, Şekil 7.5’te Treap 164 (x, p) sıralı ikili değerlerinin İkili Arama Ağacı’na eklenmesiyle oluşturulabilir. Öncelikler rasgele seçilmiş olduğundan bu, anahtarların rasgele permütasyonunu almaya eşdeğerdir – bu durumda permütasyon elemanlarını İkili Arama Ağacı’na eklemek olur. Böylece oluşturulan Treap şekli, Rasgele İkili Arama Ağacı ile aynıdır. Özellikle, her x anahtarını sırası ile yer değiştirirsek16, Önerme 7.1 geçerlidir. Önerme 7.1 Treap bakımından yeniden düzenlenerek şu hale gelir: Önerme 7.2. n anahtardan oluşan bir küme eleman dizisini, S, depolayan bir Treap, için aşağıdaki cümleler geçerlidir: 1. Her x ∈ S için, x için arama yolunun beklenen uzunluğu 2. Her x ∉ S için, x için arama yolunun beklenen uzunluğu r(x) burada S olur. olur. ∪ {x} içinde x sırasını gösterir. Yine, vurgulamalıyız ki Önerme 7.2’de beklenti, her düğüm için rasgele öncelik seçimleri üzerinden belirlenir. Anahtardaki rastgelelik hakkında herhangi bir varsayım gerektirmez. Önerme 7.2 Treaps veri yapısının bul(x) işlemini verimli olarak uygulayabileceğini anlatır. Ancak, Treaps veri yapısının gerçek yararı ekle(x) ve sil(x) işlemlerini desteklemesidir. Bunu yapmak için, Yığın özelliğini korumak için bir döndürüşün gerçekleştirmesine ihtiyaç vardır. Bkz. Şekil 7.6. İkili Arama Ağacı’nda bir döndürüş, İkili Arama Ağacı özelliği korunarak, w düğümünün menşei olan u üzerinde yerel bir değişiklik yaparak u menşei olarak w düğümünü belirlemeyi gerektirir. İki çeşit döndürüş vardır: w düğümünün sol veya sağ çocuk olmasına bağlı olarak sırasıyla, sol döndürüş ve sağ döndürüş. 16 x elemanlarından oluşan bir küme eleman dizisi, S, içindeki bir x elemanının sırası x 'den az olan, S elemanlarının sayısıdır. 165 Bunu gerçekleştiren kod bu iki olasılığı işlemek zorundadır ve (u kök olduğunda) sınır değerlere dikkat edilmelidir, böylece ortaya çıkan gerçek kodun uzunluğu Şekil 7.6’da gösterildiğinden uzundur. 166 Treap u veri yapısı açısından, bir döndürüşün en önemli özelliği w derinliğinin bir azalması ve derinliğinin bir artmasıdır. Döndürüşleri kullanarak, ekle(x) işlemini aşağıdaki gibi uygulayabilirsiniz: Yeni bir u düğümü oluştururuz, u.x = x olarak belirleriz, ve u.p için rasgele değer atarız. Bunu takiben, İkili Arama Ağacı için olağan ekle(x) algoritmasını kullanarak x eklenir. u şimdi Treap veri yapısının yaprağı olmuştur. Bu noktada Treap, İkili Arama Ağacı’nın özelliğini yerine getirmektedir, ancak Yığın özelliğini sağlaması hakkında kesin bir şey söylemeyiz. Özellikle, u.parent.p > p u durumu söz konusu olabilir. Bu durumda ise, w=u.parent iken w’nin menşei olacak şekilde bir döndürüş gerçekleştirilir. u, Yığın özelliğine aykırı olmaya devam ederse, her tekrarlamada u derinliği bir azaltılarak, u kök olana kadar veya u.parent.p < u.p olana kadar döndürüş tekrarlanır. ekle(x) işleminin bir örneği, Şekil 7.7’de gösterilmiştir. ekle(x) işleminin çalışma süresi x için yapılan arama yolunu takip için gereken süre ve yeni eklenen, u, düğümünün Treap içinde onu doğru olması gerektiği yere kadar taşımak için gerçekleştirilen döndürüş sayısı ile verilir. Önerme 7.2 ile belirtilen arama yolunun beklenen uzunluğu en fazla 2 ln n + O(1) olarak belirlenmiştir. Ayrıca, her bir döndürüşün u derinliğini azalttığı biliniyor. u kök olduğunda döndürüş sona ermelidir, bu nedenle beklenen döndürüş sayısı, gerçekleştirilen arama yolunun beklenen uzunluğundan fazla olmaması gerekir. Bu nedenle, bir Treap tarafından gerçekleştirilen ekle(x) işleminin beklenen çalışma süresi O(log n) olur. (Alıştırma 7.5 bir ekleme sırasında gerçekleştirilen beklenen döndürüş sayısının O(1) olduğunu göstermenizi istiyor.) 167 Bir Treap içinde sil(x) işlemi ekle(x) işlemi ile zıt mantığı çalıştırmalıdır. Bunun için, x içeren, u, düğümünü ararız. Bunu takiben u bir yaprak haline gelinceye kadar, u düğümünü aşağı yönde hareket ettirecek döndürüşleri gerçekleştiririz. Treap yapısından u çıkarılarak u’ya bağlı düğümler birbirine uç uça eklenir. Dikkat edin, u düğümünü aşağı yönde hareket ettirmek için u düğümünü sırasıyla u.left veya u.right ile yer değiştiren sol veya sağ döndürüşü gerçekleştirebilirsiniz. Aşağıdakilerden hangisi önce geçerliyse, seçim ona göre yapılır: 1. u.left ve u.right her ikisi de null ise, o zaman u bir yapraktır ve hiçbir döndürüş gerçekleştirilmez. 2. u.left (veya u.right) null ise, u düğümünden sağa doğru (veya sırasıyla, sola doğru) döndürüş yapın. 3. u.left.p < u.right.p (veya u.left.p > u.right.p) ise, sağa doğru (veya sırasıyla, sola doğru) döndürüş yapın. 168 Bu üç kural Treap bağlantılarının koruyarak, u düğümü silindiğinde Yığın özelliğinin tekrar kazanılmasını sağlar. sil(x) işleminin bir örneği, Şekil 7.8’de gösterilmiştir. sil(x) işleminin çalışma süresini analiz etmekte gerekli olan beceri, bu işlemi ekle(x) işleminin zıttı olarak fark edebilmektir. Özellikle, aynı öncelikte u.p kullanarak x değerini yeniden eklemek isteseydik, ekle(x) işlemi aynı sayıda tam döndürüş yapardı ve Treap özelliklerini sil(x) işleminin gerçekleşmesinden önceki duruma getirerek yeniden korurdu. (Aşağıdan-yukarıya-doğru okumada Şekil 7.8, bir Treap içine 9 değerinin eklenmesini göstermektedir.) n boyutundaki Treap için sil(x) işleminin beklenen çalışma zamanı n – 1 boyutundaki Treap için ekle(x) işleminin beklenen çalışma zamanı ile orantılıdır. sil(x) işleminin beklenen çalışma zamanı O(log n) olur. 169 170 171 7.2.1 Özet Aşağıdaki teorem Treap veri yapısının performansını özetler: Teorem 7.1: Treap, Sıralı Küme arayüzünü uygular. Treap işlem başına O(log n) beklenen zamanda çalışan ekle(x), sil(x) ve bul(x) işlemlerini destekler. Treap veri yapısını Sıralı Küme Sekme Listesi ile karşılaştırmak dikkate değerdir. Her ikisi de O(log n) zamanda işlem destekliyor. Her iki veri yapısında da ekle(x) ve sil(x) işlemleri arama gerçekleştirimini takiben sabit sayıda işaretçi değişikliğini içerir (Bkz. Alıştırma 7.5). Bu nedenle, her iki yapı için, performans üzerine etki eden en kritik değer arama yolunun beklenen uzunluğudur. Sıralı Küme Sekme Listesi’nde arama yolunun beklenen uzunluğu iken, Treap için bu sayı olur. Bu durumda, Treap kısa arama yolunun getirdiği performansla birlikte Sekme Liste’lerine göre yüksek hızlı işlem gerçekleştirme becerisine sahiptir. Bölüm 4’te verilen Alıştırma 4.7 bir Sekme Listesi’nin arama yolunun beklenen uzunluğunun nasıl önyargılı para fırlatma aracılığıyla sayısına azaltılabileceğini gösterir. Bu iyileştirme ile olsa bile, Sıralı Küme Sekme Listesi’nin beklenen arama uzunluğu yolu Treap ile karşılaştırıldığında uzundur. 172 7.3 Tartışma ve Alıştırmalar Rasgele ikili arama ağaçlar hakkında birçok çalışma yapılmıştır. Devroye [19] Önerme 7.1’in kanıtını ve ilgili sonuçlarını vermektedir. Daha kuvvetli sonuçlardan en etkileyici olan başlıcası Rasgele İkili Arama Ağacı’nın beklenen yüksekliğini bir formülle ifade eden Reed [64] tarafından literatürde bulunmaktadır. Reed, rasgele bir ikili arama ağacının beklenen yüksekliğinin olduğunu gösteriyor. denklemlerinin [2, ∞] aralığındaki ve eşsiz çözümü olarak α ≈ 4,31107 olarak kaydedilmiştir. Ağacın yüksekliği bir rasgele değişken olduğu takdirde, bu değişkenin ortalama değerinden sapmalarının karelerinin ortalaması olarak tanımlanan değişinti [Kaynak: http://www.bilisimsozlugu.net/variance] düşünüldüğünde, yükseklik -rasgele değişkeninin- ortalaması etrafındaki saçılımın sabit olduğu gösterilmiştir. Treap adı Seidel ve Aragon [67] tarafından bilime kazandırılırken, Treap ve bazı varyasyonları tartışıldı. Ancak, temel yapısı Vuillemin [76] tarafından çok daha önceden Kartezyen Ağaçlar olarak literatüre geçmiştir. Treap veri yapısı için olası bir alan-optimizasyonu her düğümdeki öncelikli p alanının açıkça depolanmasının ortadan kaldırılması olabilir. Bunun yerine, bir, u, düğümünün önceliği, u adresini bellekte karma ederek hesaplanır. (32-bit Java’da, bu, u. hashCode() kullanılarak karma yapmaya eşdeğerdir). Bir dizi karma fonksiyonu muhtemelen bu uygulama için iyi çalışacaktır, ancak Önerme 7.1’in kanıtının önemli parçalarının geçerli kalması için karma fonksiyonu randomize edilmelidir, ve en küçüklük-bağlamında bağımsızlık özelliği olmalıdır: Ayrı x1, …, xk değerleri için h(x1), …, h(xk) karma değerlerinin her biri yüksek bir olasılıkla ayrı olmalıdır, ve her i ∈ {1, …, k} için bazı c sabiti vardır ki, doğru olsun. Bu tür karma fonksiyonlarının uygulaması kolay ve oldukça hızlı olan bir sınıfı listeleme karmadır (Bölüm 5.2.3). Her düğüm noktasında öncelikleri saklamayan başka Treap 173 değişimi Martinez ve Roura tarafından Randomize İkili Arama Ağacı [51] olarak bilime kazandırılmıştır. Bu varyasyonda, her u düğümünde kökenli altağacın boyutu u.boyut ile tanımlıdır. ekle(x) ve sil(x) algoritmaları randomizedir. u kökenli alt ağaca x elemanını eklemek için algoritmanın çalışma anlatımı 2 safhada incelenir: 1. 1/(boyut(u)+1) olasılığıyla x değeri her zamanki gibi yaprak olarak eklenir, ve x bu ağacın köküne getirilene kadar döndürüş yapılır. 2. Aksi takdirde, 1 – 1/(boyut(u)+1) olasılığıyla x değeri özyinelemeli olarak u.left veya u.right kökenli ağaçlardan uygun olan ağaca eklenir. Birinci durumda Treap, x düğümünün u altağacındaki herhangi bir boyut(u) önceliğinden daha küçük olan bir rasgele öncelik aldığı bir ekle(x) işlemine karşılık geliyor, ve bu durum aynı olasılık ile ortaya çıkar. Randomize İkili Arama Ağacı ’ndan bir x değeri silmek, bir Treap’ten silme sürecine benziyor. x içeren u düğümü bulunur, ve art arda u derinliğini artırmak için u bir yaprak haline gelene kadar döndürüş gerçekleştirilir. Bu noktada, u çıkarılarak menşei ve çocukları uç uça birleştirilir. Her adımda sol veya sağ döndürüş gerçekleştirmek seçimi randomize edilmiştir. 1. u.left.boyut/(u.boyut – 1) olasılığıyla, u kökenli altağacın kökü u.left olacak şekilde sağ döndürüş gerçekleştirir. 2. u.right.boyut/(u.boyut – 1) olasılığıyla, u kökenli altağacın kökü u.right olacak şekilde sol döndürüş gerçekleştirir. Treap siliş algoritmasında bu olasılıklar dahilinde sol veya sağ döndürüş gerçekleşmesi kolayca doğrulanabilir. Treap veri yapısı ile karşılaştırıldığında, Randomize İkili Arama Ağaç’larının eleman ekleme ve silme yaparken birçok rasgele seçimler yapması dezavantajdır ve altağaçların boyutları korunmalıdır. Bir elemana erişim zamanı altağaç boyutu cinsinden formüle edilebilir. Treap veri yapısının beklenen erişim zamanının O(log n) olduğu avantaj olarak belirlenmiştir. (bkz.Alıştırma 7.10). Treap düğümlerinde depolanan rasgele önceliklerin ise Treap dengesini sağlamak dışında bir getirisi bulunmamaktadır. 174 Alıştırma 7.1. Şekil 7.5’te gösterilen Treap içine (7 öncelikli) 4,5 eklemeyi ve daha sonra (20 öncelikli) 7,5 eklemeyi açıklayın. Alıştırma 7.2. Şekil 7.5’te gösterilen Treap içinden 5 ve 7 değerlerinin silinmesini açıklayın. Alıştırma 7.3. Şekil 7.1’in sağ tarafında bulunan ağacı oluşturan 21.964.800 dizi olması size yetkin ve yeterli bir değerlendirme gibi mi görünüyor? Kanıtlayın. (İpucu: h yüksekliği cinsinden bir özyinelemeli formül bulun, sonucu ikili ağacı oluşturacak dizilerin sayısı olsun. h = 3 için formülünüzü sınayın.) Alıştırma 7.4. n farklı değerleri sırası rasgele değiştirilmiş şekilde içeren a dizisini girdi olarak alan sırasınıDeğiştir(a) yöntemini tasarlayın ve uygulayın. Yöntemin çalışma zamanı O(n) olmalıdır ve kanıtlamalısınız ki a dizisinin her bir n! olası permütasyonuna eşit olasılıkta rastlanır. Alıştırma 7.5. Önerme 7.2’nin her iki bölümünü de kullanarak ekle(x) ve sil(x) işlemiyle gerçekleştiren döndürüşlerin beklenen sayısı O(1)’dir. Alıştırma 7.6. Öncelikleri açıkça depolamayan bir Treap versiyonunu geliştirin. Her düğüm hashCode() ile karma edildikten sonra önceliği işleme koyma becerisine sahip olmalıdır. Alıştırma 7.7. Varsayın ki, İkili Arama Ağaç içindeki her u düğümünde, u kökenli altağacın yüksekliği u.yükseklik ve u kökenli altağacın büyüklüğü, u.boyut depolansın. 1. u düğümünde sol ve sağ döndürüş gerçekleştirildiğinde, bu iki değişkenin döndürüşten etkilenen tüm düğümler için sabit sürede güncellenebildiğini gösterin. 2. Aynı sonucun, her u düğümü için, u derinliğini de depolamaya çalıştığınız takdirde, neden geçerli olmadığını açıklayın. Alıştırma 7.8. En kötü O(n) zamanında çalışan, a adında eleman sayısı n olan sıralı dizinin elemanlarını Treap içine ekleyerek oluşturan bir algoritma tasarlayın ve gerçekleştirin. a elemanları ekle(x) yöntemi ile teker teker eklenseydi, oluşan Treap sizin oluşturduğunuz Treap ile aynı olmalıdır. 175 Alıştırma 7.9. Bu alıştırma Treap içinde aradığınız düğüme yakın bir işaretçinin verildiğinde verimli arama yapmanızın ayrıntılarını araştırmada size yardımcı olacaktır. 1. Bir Treap içindeki her düğümün altağacındaki en küçük ve en büyük değerleri depoladığı bir uygulama tasarlayın ve uygulayın. 2. Bu ekstra bilgiyi kullanarak, u düğümüne bir işaretçi yardımıyla bul(x) yöntemini çalıştıran parmakBul (x, u) yöntemini ekleyin. (x içeren düğümden fazla uzak mesafede olmaması tercih edilir.) Bu işlem u düğümünde başlar ve w.min ≤ x ≤ w.max sağlayacak bir w düğümüne ulaşıncaya kadar yukarıdan ilerler. Bu noktadan itibaren, x için w düğümünden başlayarak standart bir arama yapar. (Gösterilebilir ki, Treap içinde bulunan elemanların sayısı r ile ifade edildiğinde, r değeri x ve u.x arasında olmalıdır, parmakBul(x, u) işleminin çalışma zamanı O(1 + log r) olur. 3. Uygulamanızı tüm bul(x) işlemlerini bul(x) işleminin en son çağrıldığında bulduğu düğümden başlayarak aramayı gerçekleştiren bir Treap versiyonuna doğru genişletin. Alıştırma 7.10. Treap içinde i’nci sıradaki anahtarı döndüren al(i) işlemini içeren Treap versiyonu tasarlayıp uygulayın. (İpucu: Her, u, düğümü u kökenli altağacın boyutunu depolar.) Alıştırma 7.11. Liste arayüzünün Treap içinde gerçekleştirildiği TreapList adında bir veri yapısını uygulayın. Treap içindeki her düğüm bir Liste arayüzünü gerçekleştirir. Treap için yapılan içsel-sıralamada ziyaret edilen elemanlar aynı sırada, Liste’de göründüğü sırada, görünür. Tüm Liste işlemlerinin, al(i), belirle(i, x), ekle(i, x) ve sil(i), beklenen çalışma zamanları O(n) olmalıdır. Alıştırma 7.12. böl(x) işlemini destekleyen bir Treap versiyonunu tasarlayın ve uygulayın. Bu işlem, Treap içindeki x’den daha büyük olan tüm değerleri siler ve tüm silinen değerleri içeren ikinci bir Treap döndürür. Örnek: t2 = t.böl(x) kodu t içinden x’den daha büyük tüm değerleri ayırır ve yeni Treap t2 olarak döndürülür. böl(x) işleminin beklenen çalışma zamanı O(log n) olmalıdır. Uyarı: boyut() yönteminin sabit zamanda çalışmasına izin verecek şekilde bu değişikliğin doğru çalışması için Alıştırma 7.10’da belirtilen değişiklikleri uygulamak gereklidir. 176 Alıştırma 7.13. böl(x) işleminin tersi olarak düşünülebilen em(t2) işlemini destekleyen Treap versiyonunu tasarlayın ve uygulayın. Bu işlem Treap, t2, içindeki tüm değerleri siler ve alıcıya ekler. Bu işlemin öngerektirdiği önerme t2 içindeki en küçük değer alıcı içindeki en büyük değerden daha büyüktür olur. em(t2) işlemi beklenen O(log n) zamanda çalışmalıdır. Alıştırma 7.14. Martinez'in Randomize İkili Arama Ağaç’larını gerçekleştirin. Treap ile yapılan bir uygulamanın performansını sizin yapmış olduğunuz uygulamanın performansı ile karşılaştırın. 177 Bölüm 8 Günah Keçisi Ağaçları Bu bölümde, Günah Keçisi Ağacı adında İkili Arama Ağacı veri yapısı tanıtılıyor. Bu yapı, ortak akla dayalıdır: Bir şey yanlış giderse, insanların yapmak istedikleri ilk şey birilerini suçlamaktır (günah keçisi). Suçlama sonucu, sorunu çözmek için günah keçisi yalnız bırakılır. Bir Günah Keçisi Ağacı, kısmi-yeniden-oluşturma işlemleri karşısında dengeli davranır. Kısmi-yeniden-oluşturma işlemi sırasında, bütün altağaç parçalara ayrılır ve mükemmel dengeli bir alt ağaç için yeniden oluşturulur. u-düğümü kökenli mükemmel dengeli bir altağacı yeniden oluşturmanın birçok yolu vardır. u altağacını ziyaret etmek, bir diziye bütün düğümleri toplamak, ve sonra özyinelemeli olarak dengeli bir altağacı oluşturmak basit olan yoldur. m = a.uzunluk/2 ise, a[m] yeni ağacın kökü olur, a[0],…,a[m–1] özyinelemeli olarak sol altağaçta depolanır, a[m+1],…,a[a.uzunluk–1] özyinelemeli olarak sağ altağaçta depolanır. yeniden_oluştur(u) için yapılan bir çağrının çalışma zamanı O(boyut(u)) olur. Elde edilen altağaç minimum yüksekliğe sahiptir; boyut(u) düğümüne sahip olan ağaçlardan daha az yüksekliğe sahip hiçbir ağaç yoktur. 8.1 Günah Keçisi Ağacı: Kısmi Yeniden Oluşturmalı İkili Arama Ağacı Depolanan düğüm sayısı, n, ve düğüm sayısına üst sınır sağlayan bir değişken, q ile belirtildiği takdirde ortaya çıkan İkili Arama Ağacı’na Günah Keçisi Ağacı denir. 178 Her zaman için, n ve q, aşağıdaki eşitsizliği sağlar: Buna ek olarak, Günah Keçisi Ağacı’nın logaritmik özelliğe sahip olan yüksekliği aşağıdaki değeri hiçbir zaman geçmez: 179 Hatta bu sayılarla bile, Günah Keçisi Ağacı’nın dengesi sınırlanamaz. Şekil 8.1' deki ağaç q = n = 10 bul(x) ile tanımlanır ve yüksekliği için geçerli eşitsizlik 5 < log3/210 ≈ 5,679 olur. işlemini Günah Keçisi Ağacı’nda gerçekleştirmek için İkili Arama Ağacı’nın standart arama algoritması kullanılır (bkz. Bölüm 6.2). Yükseklik ile orantılı olduğu için bul(x) işleminin çalışma zamanı (8.1) koşulu nedeniyle O(log n) ile sınırlanır. ekle(x) işlemi için n ve q değişkenlerinin değeri artırılır. İkili Arama Ağacı için gerçekleştirilen algoritma Günah Keçisi Ağacı için de bu artırım yapıldıktan sonra aynen geçerlidir. Önce x aranır, u için u.x = x belirlenimi yapıldıktan sonra, u yeni bir yaprak olarak ağaca eklenir. Bu noktada eğer şanslı olabilirsek, u derinliği log3/2q sayısını aşmayacaktır. Eğer öyleyse, Günah Keçisi Ağacı daha fazla değiştirilmez. Ne yazık ki, bazen derinlik(u) > log3/2q olarak saptanabilir. Bu durumda, çok fazla zahmet gerektirmeyen iş, u düğümünün bulunmasıdır. Derinliği log3/2q sayısını aşan u düğümünden başlayarak köke doğru ilerleriz. Günah keçisi w bulunmalıdır. Günah Keçisi Ağacı’nın yüksekliğini yeniden log3/2q yapmak için u düğümü eklendikten sonra, w altağacı yeniden oluşturulur. w kökenli altağaç tamamen yok edilir, yükseklik azaltılır ve mükemmel dengeli İkili Arama Ağacı oluşturulur. 180 Günah keçisi, w, çok dengesiz düğümdür. Şu özelliğe sahiptir: w.child kökte u düğümüne giden yolda w düğümünün çocuğudur. u eklenmeden önce (8.2) gösteriyor ki, w altağacı mükemmel dengeli İkili Ağaç değildi, bu nedenle w yeniden oluşturulduğunda, yükseklik en az 1 azalır, böylece Günah Keçisi Ağacı’nın yüksekliği yeniden log3/2q olur. Günah Keçisi, w diye adlandırdığımızda, w kökenli altağacı yeniden oluşturmanın maliyetini dikkate almazsak, ekle(x) işleminin çalışma zamanı O(log q) = O(log n) olur. Çalışma zamanına etki eden en büyük faktör yapılan ilk aramadır. Günah Keçisi’ni bulmak ve yeniden oluşturmak işleminin maliyeti sonraki bölümde amortize analiz tekniği kullanılarak inceleniyor. Bir Günah Keçisi Ağacı içinde sil(x) uygulaması çok basittir. sil(x) işlemini Günah Keçisi Ağacı’na uygulamak için önce x aranır, sonra İkili Arama Ağacı’nın standart siliş algoritması kullanılarak x silinir. (Unutmayın ki, ağacın yüksekliği bu işlem sonrasında artmaz.) Sonra, n azaltılır, q aynen bırakılır. En sonunda, q > 2n eşitsizliği kontrol edilir, ve sağlıyorsa tüm ağacın mükemmel dengeli İkili Arama Ağacı haline gelmesi için yeniden oluşturma ile q=n olarak belirlenir. 181 Yine, yeniden oluşturma maliyeti dikkate alınmayarak, sil(x) işleminin çalışma zamanı ağacın yüksekliği ile orantılıdır, ve bu nedenle O(log n) olur. 182 8.1.1 Doğruluk Analizi ve Çalışma Zamanı Bu bölümde Günah Keçisi Ağacı işlemlerinin doğruluğunu ve amortize çalışma zamanlarını analiz edeceğiz. Doğruluk kanıtlaması için ilk olarak ekle(x) işlemi (8.1) koşuluna itiraz eden bir düğümde sonuçlanırsa, her zaman bir günah keçisi bulunabileceğini göstermemiz gerekiyor: Önerme 8.1. Günah Keçisi Ağacı’na eklenmiş u düğümünün derinliği h > log3/2 q ile verilsin. u’dan köke giden yol üzerinde bulunan w düğümü için geçerli koşul şudur: Kanıt. Çelişki yaratmak ve sınamak için u’dan köke giden yol üzerinde bulunan her w düğümü için bu durumun böyle olmadığını varsayalım. Kökten u düğümüne giden yol r = u0, …, uh = u ile ifade edildiği takdirde, size(u0) = n, ve daha genel olarak, Çelişkilidir, çünkü boyut(u) ≥ 1 olduğu için buna bağlı olan bağıntılar vardır: Şimdi, çalışma zamanını oluşturan henüz açıklanmamış bileşenleri analiz edeceğiz. İki bölüm içeriyor: Günah keçisi düğümlerini ararken boyut(u) aramalarının maliyeti, ve günah keçisi, 183 w, bulunduğunda yeniden_oluştur(w) için yapılan aramaların maliyeti. boyut(u) için yapılan aramaların maliyeti, yeniden_oluştur(w) için yapılan aramaların maliyeti ile aşağıdaki gibi ilişkilendirilebilir: Önerme 8.2. Bir Günah Keçisi Ağacı içinde ekle(x) çağrısı sırasında w günah keçisini bulma ve w kökenli altağacı yeniden oluşturmanın maliyeti O(boyut(w)) olur. Kanıt. w düğümünü bulduktan sonra günah keçisini yeniden oluşturmanın maliyeti O(boyut(w)) olur. Günah keçisi düğümünü ararken u0, ..., uk düğümlerinden oluşan dizi için günah keçisi uk = w bulunana kadar boyut(u) çağrılır. Fakat, dizideki günah keçisi olan ilk düğümün uk olması nedeniyle her i ∈ {0, …, k–2} için bilinmelidir ki, Bu nedenle, boyut(u) için yapılan tüm çağrıların maliyeti olur. Toplamın azalan bir geometrik dizi olması gerçeğinden yola çıkılarak son satırda kanıt tamamlanabildi. 184 Önerme 8.3. Boş bir Günah Keçisi Ağacı ile başlandığında, ekle (i, x) ve sil (i) işlemleri toplam m defa çağrılırsa, tüm yeniden_oluştur(u) çağrıları için gerekli toplam çalışma süresi O(m log m) olur. Kanıt. Bunu kanıtlamak için, kredili ağaç geliştirilmiş olup bu, kredilerin yeniden_oluştur(u) için gerekli olanakları sağladığını göstermeye dayanıyor. Her düğümde c kredilik bir sayı depolanırsa, yeniden oluşturma için harcanan zamanda her kredi için, bazı sabit, c ödemeleri gerçekleştirilebilir. Bu temel O(m log m) toplam kredi verir ve yeniden oluşturma için yapılan her çağrı, u tarafından depolanan krediden düşer veya krediye eklenir. Her düğüm 1 kredi içerirse, işlem başına log3/2 q ≤ log 3/2 m kredi elde olur. Her düğümün teker teker işleme konduğunu düşünürseniz, toplamda O(m log m) kredi verir. Geriye, bu kredilerin yeniden_oluştur(u) için gerekli krediyi sağladığını göstermek kalıyor. Ekleme sırasında genelleme kaybına girmeden varsayalım ki, Gerçeğini kullanarak bu anlaşılmalıdır ki, ve bu nedenle Şimdi, u içeren bir altağaç son kere yeniden oluşturulduğunda (ya da u eklendiği zaman u içeren bir altağaç, hiçbir zaman yeniden oluşturulmamışsa) 185 elde olur. Bu nedenle, u.left ve u.right altağaçlarını etkileyen ekle(x) sayısı ya da sil(x) işlem sayısı Böylece u düğümünde yeniden_oluştur(u) çağrısı için gerekli O(boyut(u)) zamanına yetecek kadar en az bu kadar kredimiz vardır. Silme sırasında yeniden_oluştur(u) çağrıldığında q > 2n doğrudur. q – n > n doğrudur. Bu krediye sahibiz ve depolarız. Kökü yeniden oluşturmak için gerekli O(n) zamanı tüketmek için bu kredi yeterlidir. Bu kanıtı tamamlar. 8.1.2 Özet Teorem 8.1. Aşağıdaki teorem Günah Keçisi Ağacı veri yapısının performansını özetler: Bir Günah Keçisi Ağacı, Sıralı Küme arayüzünü uygular. yeniden_oluştur(u) işlemi için yapılan çağrıların maliyeti dikkate alınmayarak, ekle (x), sil (x) ve bul(x) işlemleri Günah Keçisi Ağacı tarafından işlem başına O(log n) zamanda desteklenir. Ayrıca, boş bir Günah Keçisi Ağacı ile başlandığında, ekle (x) ve sil (x) işlemleri toplam m defa çağrılırsa, tüm yeniden_oluştur (u) çağrıları için gerekli toplam çalışma süresi O(m log m) olur. 186 8.2 Tartışma ve Alıştırmalar Günah Keçisi Ağacı, Galperin ve Rivest [33] tarafından tanımlanmış ve analiz edilmiştir. Ancak, aynı yapı, yükseklikleri küçük olduğu sürece herhangi bir şekle sahip olabilen Genel Dengeli Ağaçlar olarak Andersson [5, 7] tarafından daha önce keşfedilmiştir. Günah Keçisi Ağacı Küme uygulaması ile yapılacak denemeler, genellikle bu kitaptaki diğer Sıralı uygulamalarından çok daha yavaş olduğunu ortaya koyacaktır. Bu biraz şaşırtıcı olabilir, çünkü yükseklik sınırı, bir Sekme Listesi’ndeki arama yolunun beklenen uzunluğundan daha iyidir ve Treap’inkinden de çok uzak değildir. Uygulama alt ağaçların boyutlarını her düğümde açıkça depolayarak ya da zaten hesaplanmış altağaç boyutlarını yeniden kullanarak (Alıştırma 8.5 ve 8.6) optimize edilebilir. Hatta bu iyileştirmelerle bile, her zaman ekle(x) ve sil(x) işlemlerinin Günah Keçisi Ağacı’ndaki çalışma zamanları diğer Sıralı Küme uygulamalarından daha uzun sürecektir. Bu performans açığı, aslında, kitapta tartışılan diğer Sıralı Küme uygulamalarının aksine, bir Günah Keçisi Ağacı’nın kendisini yeniden yapılandırırken çok zaman geçirebilmesine bağlıdır. Alıştırma 8.3 sizden bir Günah Keçisi Ağacı’nın n işlem dizisi için yapılan yeniden_oluştur(u) çağrıları için n log n zaman gerektiğini kanıtlamanızı soruyor. Bu, n işlem sırasında O(n) yapısal değişiklik yapan diğer Sıralı Küme uygulamalarının aksinedir. Bu, ne yazık ki aslında, Bir Günah Keçisi Ağacı’nın yeniden yapılandırmak için tüm çağrıları yeniden_oluştur(u) tarafından yapmasının getirdiği gerekli bir sonuçtur. [20]. Performansları iyi olmamasına rağmen, Günah Keçisi Ağacı’nın doğru seçim olabileceği uygulamalar vardır. Bu bir döndürüş yapıldığında sabit zamanda güncellenemeyen, ancak bir yeniden_oluştur(u) işlemi sırasında güncellenebilen düğümler ile ilişkili ek veriler olduğu zaman ortaya çıkabilir. Bu gibi durumlarda, Günah Keçisi Ağacı ve kısmi yeniden oluşturmaya dayalı yapılar çalışabilir. Bu tür bir uygulamanın örneği Alıştırma 8.11’de gösterilmiştir. 187 Alıştırma 8.1. Şekil 8.1 'de gösterilen Günah Keçisi Ağacı’na 1,5 ve 1,6 değerlerinin eklenmesini açıklayın. Alıştırma 8.2. Boş bir Günah Keçisi Ağacı’na 1, 5, 2, 4, 3, dizisi eklendiğinde ne olacağını açıklayın. Önerme 8.3’ün kanıtında kredilerin nereye gittiğini ve bu ekleme dizisi sırasında nasıl kullanıldıklarını gösterin. Alıştırma 8.3. Boş bir Günah Keçisi Ağacı ile başlandığında ve x = 1, 2, 3, ... n için ekle(x) çağrıldığında, yeniden_oluştur(u) için yapılan tüm çağrıların çalışma zamanının bazı sabit c>0 için en az cn log n olduğunu gösterin. Alıştırma 8.4. Bu bölümde anlatıldığı gibi, Günah Keçisi Ağacı, arama yolunun log3/2q uzunluğunu aşmamasını garanti eder. 1. 1 < b < 2 olan bir b parametresi için arama yolunun uzunluğu logbq değerini aşmayan, Günah Keçisi Ağacı’nın değiştirilmiş bir sürümünü tasarlayın, analiz edin ve uygulayın. 2. Analiziniz ve/veya deneyleriniz, n ve b cinsinden, bul(x), ekle(x) ve sil(x) amortize maliyetleri hakkında ne diyor? Alıştırma 8.5. Günah Keçisi Ağacı’nın ekle(x) yöntemini, zaten hesaplanmış olan alt ağaçların boyutlarını tekrar hesaplamak için zaman harcamayacak şekilde, değiştirin. Bu mümkündür çünkü yöntem boyut(w) değerini hesaplamak istediği zaman zaten boyut(w.left) ya da boyut(w.right)’dan birini hesaplamış olması gerekir. Değiştirdiğiniz uygulama ile burada verilen uygulamanın performansını karşılaştırın. Alıştırma 8.6. Her bir düğüm noktasında o düğümde kökenli altağacın boyutlarını depolayan ikinci bir Günah Keçisi Ağacı veri yapısı versiyonunu uygulayın. Bu uygulama ile elde edilen performansı orijinal Günah Keçisi Ağacı uygulamasının yanı sıra Alıştırma 8.5’in uygulaması ile de karşılaştırın. Alıştırma 8.7. Bu bölümün başında tartışılan yeniden_oluştur(u) yöntemini yeniden oluşturulacak altağacın düğümlerini depolamak için bir dizi kullanılmasını gerektirmeyecek şekilde yeniden uygulayın. Bunun yerine, önce bir bağlantılı liste halinde düğümleri bağlamak 188 için özyineleme kullanılması ve sonra bu bağlantılı listenin mükemmel bir şekilde dengelenmiş ikili ağaca dönüştürülmesi gerekir. (Her iki adımın da çok iyi özyinelemeli uygulamaları vardır.) Alıştırma 8.8. Ağırlıklı Dengeli Ağaç’ı analiz edin ve uygulayın. Kök dışında, her u düğümü boyut(u) ≤ (2/3) boyut(u.parent) denge değişmezini korur. Bu ağacın ekle(x) ve sil(x) işlemleri standart İkili Arama Ağacı işlemleri ile aynıdır, bir farkla herhangi bir u düğümünde denge değişmezlik koşulu bozulduğunda u.parent kökenli altağaç yeniden oluşturulur. Analiziniz Ağırlıklı Dengeli Ağaç işlemlerinin amortize çalışma zamanlarının O(log n) olduğunu göstermelidir. 189 Bölüm 9 Kırmızı-Siyah Ağaçlar Bu bölümde, Kırmızı-Siyah Ağaç adında, İkili Arama Ağaç’larının logaritmik yüksekliği olan bir versiyonu tanıtılıyor. Kırmızı-Siyah Ağaç’lar, en yaygın olarak kullanılan veri yapılarından biri olarak bulunmaktadır. Pek çok kütüphane uygulamalarında, Java Koleksiyonlar Çerçevesi ve C++ Standart Şablon Kütüphanesi’nin çeşitli uygulamaları da dahil olmak üzere birincil arama yapısı olarak görünür. Aynı zamanda Linux işletim sistemi çekirdeği içinde kullanılır. Kırmızı-Siyah Ağaç’ların popülaritesi çeşitli nedenlere dayanır: 1. n değer depolayan bir Kırmızı-Siyah Ağaç en fazla 2 log n yüksekliğe sahiptir. 2. Kırmızı–Siyah Ağaç üzerinde ekle(x) ve sil(x) işlemlerinin çalışma zamanı en kötü O(log n) olur. 3. ekle(x) ve sil(x) işlemleri sırasında gerçekleştirilen döndürüşlerin amortize sayısı sabittir. Bu özelliklerin ilk ikisi zaten Kırmızı-Siyah Ağaç’larını Sekme Listeleri, Treap ve Günah Keçisi Ağaçları’nın önüne koyuyor. Sekme Listeleri ve Treap rasgelelik üzerine dayanır ve O(log n) çalışma zamanı da sadece beklenendir. Günah Keçisi Ağaç’ları kendi yüksekliğine garantili bağlıdır, ancak sadece ekle(x) ve sil(x) işlemleri O(log n) amortize zamanda çalışır. Üçüncü özellik sadece kek üzerine krema sürmeye benzer. Bu bize bir x elemanını eklemek veya kaldırmak için gerekli zamanın x’i bulmak için gerekli zaman tarafından azımsandığını söylüyor.17 Ancak Kırmızı-Siyah Ağaç’ların güzel özellikleri bedava değildir: uygulama karmaşıklığı. Yükseklik üzerindeki 2 log n sınırının sağlanması kolay değildir. Bu durum çeşitli durumların dikkatli analizini gerektirir. Uygulamanın her durumda tam olarak doğru şeyi yaptığından emin olmalısınız. Yanlış bir döndürüş veya renk değişikliği, anlaması ve izlemesi çok zor olabilir bir hata üretir. 17 Unutmayın ki, Sekme Listesi ve Treap de beklenen anlamda bu özelliğe sahiptir. Bkz. Alıştırma 4.6 ve 7.5. 190 Kırmızı-Siyah Ağaç’ların uygulamasına doğrudan atlamadan önce, ilgili bir veri yapısı hakkında biraz bilgi sağlanacaktır: 2-4 Ağaçlar. Bu kırmızı-siyah ağaçların nasıl keşfedildiğine ve neden verimli uygulanabileceklerine dair mümkün olduğunca fikir verecektir. 9.1 2-4 Ağacı 2-4 Ağacı aşağıdaki özelliklere sahip (kökü olan) bir ağaçtır: Özellik 9.1 (yükseklik). Bütün yapraklar aynı derinliğe sahiptir. Özellik 9.1 (derece). Her iç düğüm 2, 3 veya 4 çocuk babasıdır. 2-4 Ağaç’ın bir örneği Şekil 9.1'de gösterilmiştir. 2-4 Ağaç’ın özellikleri, yüksekliğinin yaprak sayısı açısından logaritmik olduğunu göstermektedir: Önerme 9.1. n yapraklı 2-4 ağacı en fazla log n yüksekliğine sahiptir. Kanıt. 2-4 Ağaç’ın yüksekliği h ise, bir iç düğümün çocuk sayısı üzerindeki alt sınır olan 2, ağacın en az 2h yaprağa sahip olduğunu anlatır. Bir başka deyişle, Bu eşitsizlikte her iki tarafın da logaritmasını almak h ≤ log n verir. 9.1.1 Yaprak eklenmesi 2-4 Ağaç’ına yaprak eklemek kolaydır (Bkz. Şekil 9.2) İkinci-son seviyede bazı düğüm w çocuğu olarak bir yaprak, u, eklemek isterseniz, w çocuğu direkt olarak u olur. Bu kesinlikle yükseklik özelliğini korur, ancak derece özelliğine uymayabilir; u eklenmeden önce w 4 çocuğu varsa; çünkü w şimdi 5 çocuğu olacaktır. Bu durumda, iki düğüm halinde w ikiye 191 bölünmelidir, w ve w’ sırasıyla iki ve üç çocuğu sahip olmalıdır. Ancak şimdi w’ için bir baba lazımdır, bu yüzden w’ özyinelemeli olarak w babasının çocuğu yapılır. Yine, bu w’ babasının çok fazla çocuğa sahip olmasına neden olabilir, bu durumda bölünür. Dört çocuktan daha az olan bir düğüme ulaşana kadar, ya da kök r, iki düğüme, r ve r’, bölünene kadar bu süreç devam eder. Sonraki durumda, r ve r’ çocuklarına sahip yeni bir kök yapılandırılır. Bu her yaprağın derinliğini artırır, ve böylece yükseklik özelliğini korur. 2-4 Ağaç’ın yüksekliği en fazla log n olacağı için, yaprak ekleme işlemi en fazla log n aşamadan sonra tamamlanır. 192 9.1.2 Yaprak silinmesi 2-4 Ağaç’tan bir yaprağı silme biraz daha zordur (Bkz. Şekil 9.3). Baba w ait bir u yaprağı silinir. u silinmeden önce w sadece iki çocuğa sahipse, o zaman w sadece bir çocukla kalır ve 193 bu durum derece özelliğine uymaz. Bunu düzeltmek için, w kızkardeşi olan w’ bakılır. w’ düğümü mutlaka ağaçta vardır çünkü w’ babası en az iki çocuğa sahiptir. w’ üç ya da dört çocuğa sahip olsaydı, çocuklarından biri w verilirdi. Şimdi w iki çocuğu vardır ve w' iki ya da üç çocuğu vardır ve işlem tamamlanır. Öte yandan, eğer w’ yalnızca iki çocuk sahibiyse, o zaman w ve w’ üç çocuğu olan tek bir w düğümü haline birleştirilir. Özyinelemeli olarak w’ babasından w’ silinir. Bu süreç u ya da kızkardeşinin ikiden fazla çocuğu olduğu durumlarda bir u düğümüne ulaşıldığında, ya da köke ulaşıldığında biter. Sonraki durumda, kök sadece bir çocuk ile bırakılırsa, kök silinir, ve çocuğu yeni kök yapılır. Yine, bu aynı zamanda her yaprağın yüksekliğini azaltır, ve böylece yükseklik özelliğini korur. Yine, ağacın yüksekliği en fazla log n olacağı için, yaprak silme işlemi en fazla log n aşamadan sonra tamamlanır. 9.2 Kırmızı-Siyah Ağacı: 2-4 Ağacı ile Benzeşen Kırmızı-Siyah Ağaç, Ağacı’dır. her, u, düğümü kırmızı veya siyah bir renge sahip olan bir İkili Arama Kırmızı rengi 0 ve siyah rengi 1 gösterir. Kırmızı-siyah ağaç herhangi bir işlem öncesi ve sonrasında aşağıdaki iki özelliği sağlar. Her özellik, 0 ve 1 sayısal değerlere atanan kırmızı ve siyah renkler ile ilgilidir. Özellik 9.3 (siyah-yükseklik). Kökten yaprağa giden her yol üzerinde siyah düğüm sayısı aynıdır (yaprağa giden her yol üzerinde renklerin toplamı aynıdır). 194 Özellik 9.4 (kırmızı-olmayan-kenar) İki kırmızı düğüm bitişik değildir. Kök dışında, herhangi, u, düğümü için, u.colour + u.parent.colour ≥ 1 söylenebilir. Unutmayın ki, bu iki özellikten biri bozulmadan, Kırmızı-Siyah Ağaç’ın, r, köküne her zaman siyah rengi vermek mümkün olur, Kırmızı-Siyah Ağaç’ı güncelleyen algoritmalar, kökün rengini korur. Kırmızı-Siyah Ağaç’lar hakkında bir başka fikir de uç düğümleri (nil) siyah düğüm olarak belirlemektir. Bu şekilde, Kırmızı-Siyah Ağaç’ın her gerçek, u, düğümünde iki belirgin çocuğa raslanır. Kırmızı-Siyah Ağaç örneği Şekil 9.4 'de gösterilmiştir. 9.2.1 Kırmızı-Siyah Ağaçlar ve 2-4 Ağaçlar İlk bakışta Kırmızı-Siyah Ağaç’ın siyah-yükseklik ve kırmızı-olmayan-kenar özellikleri gibi sıradışı özeliklerini korumak için verimli bir şekilde güncellenebilir olması size şaşırtıcı görünebilir. Bununla birlikte, kırmızı-siyah ağaçlar 2-4 ikili ağaçların verimli bir simülasyonu olarak tasarlanmıştır. Bkz. Şekil 9.5. n düğümlü herhangi bir kırmızı-siyah ağaç, T, düşünün ve aşağıdaki dönüşümü gerçekleştirin: Her kırmızı u düğümünü kaldırın ve iki çocuğunu direkt siyah menşei’ne bağlayın. Bu dönüşüm sonrasında bir T' ağacı sadece siyah düğümlere sahip olacaktır. T' içindeki her iç düğümün, iki, üç, ya da dört çocuğu vardır: İki siyah çocuk ile başlayan siyah bir düğüm bu dönüşümden sonra iki siyah çocuk sahibi olacaktır. Bir kırmızı ve bir 195 siyah çocuk ile başlayan siyah bir düğümün, bu dönüşüm sonrasında, üç çocuğu var olacaktır. İki kırmızı çocuk ile başlayan siyah bir düğüm bu dönüşümden sonra dört çocuk sahibi olacaktır. Ayrıca, siyah-yükseklik özelliği T' içinde yapraktan-köke-giden her yolun aynı uzunluğa sahip olduğunu garanti eder. Diğer bir deyişle, T', bir 2-4 ağacıdır! 2-4 ağacının, T', n + 1 yaprağı vardır. Bu nedenle, bu ağaç en fazla log(n+1) yüksekliğe sahiptir. 2-4 ağaçta kökten yaprağa giden her yol, kırmızı-siyah ağaçta, T, kökten bir dış düğüme giden bir yol olarak düşünülüyor. Bu yolda ilk ve son düğümler siyahtır ve her iç düğümden en çok biri kırmızıdır ve bu nedenle bu yol en fazla log(n+1) siyah düğüm ve en fazla log(n+1) - 1 kırmızı düğüm içerir. Bu nedenle, T içinde kökten herhangi bir iç düğüme giden en uzun yol en fazla ile ifade edilir (n ≥ 1 için). Bu Kırmızı-Siyah Ağaç’ların en önemli özelliğidir: Önerme 9.2. n düğümlü kırmızı-siyah ağacın yüksekliği en fazla 2 log n olur. 196 2-4 Ağaç’lar ve Kırmızı-Siyah Ağaç’lar arasındaki ilişkiyi çözdüğümüz için, bir kırmızı-siyah ağaç oluşturmak ve verimli bir şekilde koruyarak bakımını yapmak için eleman eklenmesi ve silinmesi sırasında güçlük yaşanmayacaktır. İkili Arama Ağacı’na eleman eklemek yeni bir yaprak ekleyerek yapılabilir. Bunu önceden görmüştük. Öte yandan, ekle(x) işlemini Kırmızı-Siyah Ağaç’a uygulamak için 2-4 Ağaç’ın beş çocuklu bir düğümünü ortadan bir yerden ayırmayı gerçekleştirecek bir yönteme ihtiyacımız var. Beş çocuklu bir 2-4 Ağaç’ın düğümü iki kırmızı çocuktan birinin yine bir kırmızı çocuğu olan bir siyah düğümü tarafından temsil edilirse, bu düğümü kırmızı ile 197 renklendirerek "ortadan bir yerden ayırabiliriz" ve iki çocuğu siyah ile renklendiririz. Bu uygulamanın bir örneği, Şekil 9.6’de gösterilmiştir. Benzer şekilde sil(x) uygulaması için iki düğümü birleştiren ve kızkardeşten bir çocuk borçlanan bir yönteme ihtiyacımız var. İki düğümü birleştirmek, onları ortadan bir yerden bölmek ile zıt kavramlardır (Şekil 9.6’da gösterilmiştir). İki (siyah) kardeşin kırmızı ile renklendirilmesini, ve (kırmızı) menşei’nin siyah ile renklendirilmesini içerir. Bir kardeşten borçlanma için döndürüşler ve yeniden renklendirmeler gerekir, en karmaşık yönergelerden biridir. Tabii ki, tüm bunlar boyunca siyah-yükseklik ve kırmızı-olmayan-kenar özelliğini korumamız gerekiyor. Bunun yapılabilir olması şaşırtıcı değildir, dikkate alınması çok sayıda durumlar vardır. Özellikle Kırmızı-Siyah Ağaç’ı bir 2-4 Ağaç ile doğrudan benzeştirmek isterseniz, yeri gelir ki, sadece 2-4 Ağaç’ı göz ardı etmeniz ve Kırmızı Siyah Ağaç’ın özelliklerini sağlamaya çalışmanız daha kolay olur. 9.2.2 Kırmızı-Siyah Ağaçlarda Sola-dayanım Kırmızı-Siyah Ağaç’ları anlamak için sadece bir tanıma başvurmak yetersiz kalacaktır. En gerekli şart, ekle(x) ve sil(x) işlemleri sırasında siyah-yükseklik ve kırmızı-olmayan-kenar özelliklerinin korunması ve yönetilmesidir. Kırmızı-Siyah Ağaç veri yapısı bu koşulun yanı sıra bir başka koşulu da yerine getirir: Özellik 9.5 (sola-dayanım). Herhangi bir, u, düğümünde, u.left siyah ise u.right siyahtır. Şekil 9.4’de gösterilen Kırmızı-Siyah Ağaç sola-dayanım özelliğini sağlamıyor, nedeni ise en sağdaki yol üzerindeki kırmızı düğüm menşeinin kırmızı ve siyah iki çocuğu olmasıdır. Sola-dayanım özelliğini korumaktaki neden ekle(x) ve sil(x) işlemleri sırasında karşılaşılan durumların sayısını azaltmaktır. 2-4 Ağacı açısından, her 2-4 Ağacı’nın eşsiz bir şekilde temsil edildiğini anlatır: İki derecesindeki bir düğüm iki siyah çocuklu bir siyah düğüm haline gelir. Üç derecesindeki bir düğüm sol çocuğu kırmızı ve sağ çocuğu siyah olan siyah bir 198 düğüm haline gelir. Dört derecesindeki bir düğüm iki kırmızı çocuklu siyah bir düğüm haline gelir. ekle(x) ve sil(x) gerçekleştirimini ayrıntılı olarak irdelemeye başlamadan önce bu yöntemler tarafından kullanılan ve Şekil 9.7’de gösterilen bazı kolay altprogramları sunuyoruz. İlk iki altprogram siyah-yükseklik özelliğini korurken renkleri değiştirmek için kullanılır. pushBlack(u) yöntemi iki kırmızı çocuğu olan siyah bir u düğümünü girdi olarak alır, aynı ağaç üzerinde iki siyah çocuğu olan kırmızı bir düğüm haline getirir. pullBlack(u) yöntemi, bu işlemin zıttını kırmızı ve siyahı yer değiştirerek gerçekleştirir. solaÇevir(u) yöntemi u ve u.right düğümlerinin renklerini değiştirir ve sonra u düğümünde sola döndürüş gerçekleştirir. Bu yöntem, bu iki düğümün renklerinin yanı sıra menşei-çocuk ilişkisini tersine çevirir: 199 solaÇevir(u) işleminin yararı (u.left siyah ve u.right kırmızı olduğu için) sola-dayanım özelliğini bozan u düğümündeki sola-dayanımı tekrar geri kazandırmasındandır. Bu özel durumda, bu işlemin siyah-yükseklik ve kırmızı-olmayan-kenar özelliklerini de koruyacağından emin olabilirsiniz. solaÇevir(u) işlemi için sağaÇevir(u) işlemindeki tüm sağlar sol ve sollar sağ olduğu için simetriktir. 9.2.3 Ekleme Yeni bir yaprak, u, eklemek için Kırmızı-Siyah Ağaç’ın ekle(x) uygulamasında standart İkiliArama Ağacı eklemesi yaparız. u.x = x, ve u.colour = red olarak belirlenir. Unutmayın ki, ekleme herhangi bir düğümün siyah yüksekliğini değiştirmez, bu nedenle siyah-yükseklik özelliği hiçbir zaman bozulmaz. Ancak, bu işlem sola-dayanım özelliğini bozabilir (u menşei’nin sağ çocuğu olması durumunda). Bu işlem kırmızı-olmayan kenar özelliğini de bozabilir (u menşei’nin kırmızı olması durumunda). Bu özellikleri geri yüklemek için, ekleTamiri(u) yöntemini çalıştırıyoruz. 200 Şekil 9.8’de gösterilen, ekleTamiri(u) yöntemi girdi olarak kırmızı-olmayan-kenar özelliği ve/veya sola-dayanım özelliğini bozan ve rengi kırmızı olan bir u düğümü alır. Bu yöntem ile ilgili, Şekil 9.8’e başvurmadan ya da bir parça kağıt üzerinde yeniden yapmadan anlaşılması zor gözlemler aşağıda verilmiştir. (İpucu: u kendiniz, g davranışınız yerine koyabilirsiniz. g u’nun u büyükannesi olarak anılmaktadır. w ise sizin babanızdır.) ağacın kökü ise, o zaman her iki özelliği de sağlaması için rengi siyah olarak belirlenebilir. Aynı zamanda u’nun kızkardeşi kırmızı ise, o zaman u menşei kesinlikle siyahtır, bu nedenle sola-dayanım ve kırmızı-olmayan-kenar özelliklerinin her ikisi tutarlı olarak sağlanır. Aksi takdirde, işler biraz karışır. İlk olarak u menşei, w, sola-dayanım özelliğini bozduğu belirlenir, ve eğer öyleyse, solaÇevir(u) işlemi çalıştırılır ve u = w olarak belirlenir. Bu bizi iyi tanımlanmış bir duruma götürür: u, menşei w’nin sol çocuğu olur, bu nedenle şimdi w sola-dayanım özelliğini sağlar. Geriye, kırmızı-olmayan kenar özelliğini sağlama almak 201 kalıyor. Sadece w’nin kırmızı olduğu durum hakkında endişelenmemize gerek vardır, çünkü aksi takdirde u kırmızı-olmayan-kenar özelliği zaten sağlamaktadır. Henüz işimiz bitmedi. u kırmızı, w kırmızı iken, düşünün ki, u’nun büyükannesi g vardır. Bu durumda sadece u tarafından bozulan, fakat w tarafından sağlanan kırmızı-olmayan-kenar özelliği g düğümünün siyah olduğundan bizi emin kılar. g’nin sağ çocuğu kırmızı ise, o zaman sola-dayanım özelliği g’nin çocuklarının kırmızı olduğunu garanti eder ve pushBlack(g) çağrısı g rengini kırmızı ve w rengini siyah yapar. Böylece kırmızı-olmayan- kenar özelliği u düğümü için yeniden sağlanır, ancak özellik g tarafından bozulabileceği için sürecin tamamı u = g için yeniden çalıştırılır. g’nin sağ çocuğu siyah ise, sağaÇevir(g) için yapılan bir çağrı w düğümünü g düğümünün (siyah) menşei yapar, w için iki kırmızı çocuk ortaya çıkar: u ve g. Böylece u kırmızıolmayan-kenar özelliğini sağlar, ve g sola-dayanım özelliğini sağlar. Ağaç yeniden KırmızıSiyah Ağaç’tır. 202 ekleTamiri(u) yönteminin bir döngüsünün çalışma zamanı sabittir ve her bir yineleme ya işlemi bitirir ya da u lokasyonunu köke yakınlaştırır. Bu nedenle, ekleTamiri(u) yönteminin döngü sayısı O(log n) çalışma zamanı O(log n) olur. 9.2.4 Silme Tüm Kırmızı-Siyah Ağaç varyasyonlarının hepsinde sil(x) işlemi için çok karmaşık olduğu sonucuna varılmıştır. İkili Arama Ağacı’nda olduğu gibi sil(x) işlemi bir tane u çocuklu w düğümünü bulur ve w’yi silerek, w.parent = u belirlemesiyle çocuk silinen menşei’nin menşei ile uç uça birleştirilir (İpucu: u kendiniz, w ise sizin babanız yerine koyarsanız, babamı doğuran büyükannem beni evlatlık edinmiş oluyor). Burada w siyah ise, siyah-yükseklik koşulu şimdi de menşei olan w.parent tarafından bozulmuştur. Renk ayarlaması yapılarak, w.colour = u.colour belirlenir. Bu önleme geçicidir, yeterli olmayabilir: 1) Sürecin başlangıcında eğer u ve w açıkça siyahsa u.colour + w.colour = 2 (iki-kat-siyah). Fakat böyle bir renk sözkonusu olamaz (sadece 0 kırmızı ve 1 siyahtır). Öte yandan, w kırmızı olsaydı, siyah u düğümü tarafından rengi değiştirilecekti. Çözüm olamaz, 203 çünkü sola-dayanma özelliği u.parent düğümünde patlak veriyor. Ağacın geçmiş özellikleri, silmeyi de düğümlere uygun hale getirmeyi gerekli kılıyor. silTamiri(u) yöntemi bu amaçla çağrılmaktadır. silTamiri(u) yöntemi girdi olarak rengi siyah (1) iki kat siyah (2) olan bir u düğümü alır. u iki-kat-siyah ise, silTamiri(u) bir dizi döndürüş gerçekleştirir ve yeniden renklendirir. İki-katsiyah düğüm yok edilene kadar ağaç seviyelerinde yukarı çıkarılır. Bu işlem sırasında, u düğümü süreç sonunda ağacın köküne gelinceye kadar değişir. Kökte renk değiştirilebilir. Özellikle, kırmızıdan siyaha geçiş olabilir, böylece silTamiri(u) yöntemi u menşei’nin soladayanma özelliğini bozduğunu kontrol ederek bitirir, ve öyleyse düzelterek özelliği geri kazandırır. silTamiri(u) yöntemi tarafından ele alınması gereken durumlar okuyucuya takip kolaylığı sağlaması bakımından Şekil 9.9’da gösterilmiştir. Unutmayın ki, silTamiri (u) yöntemi ikikat-siyah düğümleri işlemektedir. Başvuru yapıldığı takdirde 4 durum tespit edilir: Durum 0: İşlemesi en kolay olan u kök durumudur. u siyah olarak yeniden renklendirilir (Bu kırmızı-siyah ağaç özelliklerinin hiçbirini bozmaz). Durum 1: u’nun kızkardeşi olan v kırmızı renkte başlanır. u, menşei olan w’ye göre, sol çocuktur (sola-dayanma özelliği nedeniyle). w sağa çevrilir ve döngüye devam edilir. 204 Unutmayın ki, w.parent (yeni büyükanne) sola-dayanma özelliğini bozar, sonuç itibariyle u derinliği artmıştır. Unutmayın ki, w kırmızı iken Durum 3 de işletilebilir. Eğer öyleyse, döngü en fazla bir kere sonrasında sona erer. Durum 2: u’nun kızkardeşi olan v siyah renkte başlanır. u, menşei olan w’ye göre, sol çocuktur. pullBlack(w) için yapılan bir çağrı sonrasında u siyah, v kırmızı, w siyah yada ikikat-siyah renk alır. w sola-dayanma özelliğini bozacağı için, solaÇevir(w) için yapılan bir çağrıyla düzeltilir. Bu andan itibaren, w kırmızı, v başladığımız altağacın köküdür. w’nin kırmızı-olmayan-kenar özelliğini bozduğu kontrol edilmelidir. w’nin sağ çocuğu olan q devreye sokarak bunu başarabiliriz. Eğer q siyah olarak belirlenmişse, w kırmızı-olmayan-kenar özelliğini sağlıyor denir. u = v belirlemesi sonrasında döngüye devam edilir. Aksi takdirde, q kırmızı iken, q kırmızı-olmayan-kenar özelliğini, w sola-dayanma özelliğini bozar. Sola-dayanma özelliği solaÇevir(w) için yapılan bir çağrı tarafından düzeltilir. v’nin sol çocuğu olan q, q’nin sol çocuğu olan w, q ve w kırmızı iken v siyah yada iki-kat-siyah olur. sağaÇevir(q) çağrısı q düğümünü hem v hem w düğümlerinin menşei yapar. pushBlack(q) çağrısıyla v siyah w siyah olarak belirlenir, bu sayede, q rengi v altağacı kökünün, ilk belirtilen w, rengini alır. Bu andan itibaren, iki-kat-siyah düğüm ortadan kaldırılmış ve aynı zamanda kırmızı-olmayankenar ve siyah-yükseklik özellikleri geri kazanılmış olur. Yalnız v sağ çocuğu kırmızıysa, sola-dayanma özelliği bozulmuştur. Gerekiyorsa, solaÇevir(v) için yapılan bir çağrı bu özelliği geri kazandıracaktır. 205 Durum 3: u’nun kızkardeşi siyah renkte başlanır. u, menşei olan w’ye göre, sağ çocuktur. Bu durum 2.Durum ile simetrik olduğu için benzer şekilde kodlanmalıdır. Tek fark soladayanım özelliğinin asimetrik olması ve farklı bakım arzetmesidir. Daha önce de, pullBlack(w) için yapılan bir çağrı sonucunda v kırmızı u siyah renk almıştı. sağaÇevir(w) için yapılan bir çağrı sonucunda, v altağacın kökü haline gelir. Bu andan itibaren w kırmızıdır, w’nin sol çocuğu olan q rengine göre kod iki dala ayrışır. 206 q kırmızı ise, kod 2.Durum’da olduğunun aynısı gibi işler, ve sola-dayanma özelliğini hiçbir düğümün bozma tehlikesi yoktur. 207 q siyah ise, durum daha karmaşıktır. v'nin sol çocuğunun rengi kırmızı ise, iki kırmızı çocuk sahibidir. ve iki-kat-siyah v düğümü pushBlack(v) çağrısı ile u menşei olan w’nin orijinal rengine bürünür, işlem tamamlanır. v’nin sol çocuğunun rengi siyah ise, v sola-dayanım özelliğini bozar. solaÇevir(v) çağrısı için yapılan bir çağrı ile bu özellik geri kazandırılır. v altağacında silTamiri(u) döngüsünü çalıştırmak için u = v olarak belirlenir. silTamiri(u) süreçleri için döngü çalışma zamanı sabittir. Durum 2 ve 3 bağlamında u’nun ağaçtaki yeri köke yakın yada sonlu hale gelebilir. Durum 0 (u kök olduğu için) her zaman sonlanır. Durum 1 hemen Durum 3 ile benzeşir ve sonludur. Ağacın yüksekliği en az 2 log n olduğu için silTamiri (u) döngü sayısının en çok O(log n) sonucuna varılabilir, bu nedenle silTamiri(u) süreçlerinin her birinin çalışma zamanı O(log n) olur. 9.3 Özet Aşağıdaki teorem Kırmızı-Siyah Ağaç veri yapısının performansını özetler: Teorem 9.1. Kırmızı-Siyah Ağaç, Sıralı Küme arayüzünü uygular. Kırmızı-Siyah Ağaç işlem başına O(log n) en-kötü zamanda çalışan ekle(x), sil(x) ve bul(x) işlemlerini destekler. Aşağıda ekstra bir teorem daha veriliyor: Teorem 9.2. Boş bir Kırmızı-Siyah Ağaç ile başlandığında, ekle(x) ve sil(x) işlemleri toplam m defa çağrılırsa, tüm ekleTamiri(u) ve silTamiri(u) çağrıları için gerekli toplam çalışma süresi O(m) olur. Sadece Teorem 9.2’ın kanıtını kabaca açıklayacağız. ekleTamiri(u) ve silTamiri(u) algoritmalarını 2-4 Ağacı’na bir yaprak ekleme veya silme algoritmaları ile karşılaştırdığımızda, bu özelliğin 2-4 Ağacı’ndan devralındığına kendimizi ikna edebiliriz. Özellikle, 2-4 Ağacı’nı ayırma, birleştirme ve ödünç alma için gerekli toplam zamanın O(m) olduğunu gösterebilirsek, o zaman Teorem 9.2 de gösterilmiş olur. 208 Bu teoremin 2-4 Ağaç’lar için kanıtlanmasında amortize analizin potansiyel yöntemi kullanılmıştır.18 2-4 Ağacı’ndaki bir, u, iç düğümün potansiyelini şöyle tanımlayalım: ve 2-4 Ağaç’ın potansiyeli düğümlerin potansiyellerinin toplamı olarak tanımlanır. Bölünme oluştuğunda dört çocuklu bir düğüm iki ve üç çocuklu, iki düğüm olur. Bunun anlamı, toplam potansiyel 3 – 1 – 2 = 2 kadar düşer. Birleştirme oluştuğunda, iki çocuklu iki düğüm üç çocuklu bir düğüm tarafından yer değiştirir. Bunun anlamı, toplam potansiyel 2 – 0 = 2 kadar düşer. Bu nedenle, her bölme veya birleştirme için, potansiyel iki azalır. Düğümleri bölme ve birleştirmeyi görmezden gelirsek, çocuk sayısı bir yaprağın eklenmesi veya çıkarılması ile değişen sabit sayıda düğüm vardır. Bir düğüm eklerken, potansiyeli en fazla üç artırarak çocuk sayısındaki artış bir olur. Bir yaprağın silinmesi sırasında, potansiyeli en fazla bir artırarak çocuk sayısındaki azalma bir olur ve iki düğüm toplam potansiyellerini en fazla bir artırarak borçlanma işlemine dahil olabilir. Özetlemek gerekirse, her birleştirme ve bölme potansiyeli en az iki azaltır. Birleştirme ve bölmeyi dikkate almayarak, her ekleme veya silme potansiyelin en fazla üç yükselmesine neden olur ve potansiyel her zaman sıfır yada pozitiftir. Bu nedenle, başlangıçta boş bir ağaç üzerinde yapılan m ekleme ya da silme nedeniyle bölünmelerin ve birleştirmelerin sayısı en fazla 3m/2 olur. Böylece bu analizin bir sonucu, ve 2-4 Ağaç’lar ve Kırmızı-Siyah Ağaç’lar arasındaki ilişki Teorem 9.2 ile verilmiştir. 9.4 Tartışma ve Alıştırmalar Kırmızı-Siyah Ağaç’lar ilk olarak Guibas ve Sedgewick [38] tarafından tanıtıldı. Yüksek uygulama karmaşıklığını rağmen bunlar, en sık kullanılan kütüphaneler ve uygulamaların 18 Bu yöntemin diğer uygulamaları için bkz. Önerme 2.2’nin ve Önerme 3.1’in kanıtları. 209 bazılarında bulunur. Çoğu algoritmalar ve veri yapıları ders kitapları Kırmızı-Siyah Ağaç’ların bazı varyasyonlarını dahil etmişlerdir. Andersson [6] dengeli ağaçların sola-dayanımlı ve kırmızı-siyah ağaçlara benzeyen, ancak herhangi bir düğümünün en fazla bir kırmızı çocuğa sahip olması ek kısıtlamasına sahip bir versiyonunu açıklıyor. Bu ağaçlar 2-4 Ağaç’lar yerine 2-3 Ağaç’lar gibi davranırlar. Bunlar bu bölümde sunulan Kırmızı-Siyah Ağaç yapısından önemli ölçüde daha kolaydır. Sedgewick [66] sola-dayanan Kırmızı-Siyah Ağaç’ların iki versiyonunu açıklıyor. Bunlar 2-4 Ağaç’ları yukarıdan-aşağıya bölen ve birleştiren özyinelemeli bir simülasyon kullanır. Bu iki tekniğin kombinasyonu özellikle kısa ve zarif kod yapar. İlgili ve daha eski bir veri yapısı AVL ağacı [3]’dır. AVL Ağaç’larının yüksekliği-dengelidir: Her düğüm, u, noktasındaki u.left kökenli ağacın yüksekliği ile u.right kökenli altağaç en fazla bir farklıdır. Hemen şunun farkına varmalısınız ki, yaprakları en az sayıda olan yüksekliği h bir F(h) ağacı Fibonacci Yinelemesi’ne uyar. F(0) = 1 ve F(1) = 1 olmak üzere, Bunun anlamı, altın oran iken F(h) yaklaşık olarak eşittir. (Daha yüksek doğrulukla, ) Önerme 9.1 kanıtında olduğu gibi, Böylece AVL Ağaç’ları Kırmızı-Siyah Ağaç’lardan daha küçük yüksekliğe sahiptir. Yükseklik dengeleme ekle(x) ve sil(x) işlemleri sırasında köke giden yolu geri katederek ve her u düğümünde bir u’nun sol ve sağ altağaçlarının yüksekliği arasındaki fark iki olduğu takdirde dengelenme işlemi gerçekleştirerek korunabilir. Bkz.Şekil 9.10. Kırmızı-Siyah Ağaç’ların Andersson varyasyonu, Kırmızı-Siyah Ağaç’ların Sedgewick varyasyonu, ve AVL Ağaç’ları, burada tanımlanan Kırmızı-Siyah Ağaç yapısını uygulamaktan daha kolaydır. Ne yazık ki, bunların hiçbiri dengelenme için harcanan çalışma süresinin 210 güncelleme başına O(1) olmasını garanti edemez. Özellikle, o yapılar için Teorem 9.2’nin hiçbir analoğu yoktur. Alıştırma 9.1. Şekil 9.11’de gösterilen Kırmızı-Siyah Ağaç için hangi 2-4 Ağaç oluşturulabilir? Alıştırma 9.2. Şekil 9.11’de gösterilen Kırmızı-Siyah Ağaç içine 13, daha sonra 3,5 ve 3,3 değerlerini eklemeyi gösterin. Alıştırma 9.3. Şekil 9.11’de gösterilen Kırmızı-Siyah Ağaç içinden 11, daha sonra 9 ve 5 değerlerini silmeyi gösterin. Alıştırma 9.4. Oldukça büyük n değerleri için, gösterin ki, 2 log n – O(1) yüksekliğinde, n düğümlü Kırmızı-Siyah Ağaç’lar vardır Alıştırma 9.5. pushBlack(u) ve pullBlack(u) işlemlerini dikkate alın. Bu işlemler KırmızıSiyah Ağaç tarafından benzeştirilen 2-4 ağaca ne yapıyor? Alıştırma 9.6. Oldukça büyük n değerleri için, gösterin ki, bir dizi ekle(x) ve sil(x) işlemleri vardır ki, bunların çalıştırılması bizi n düğümden oluşan 2 log n – O(1) yüksekliğinde, Kırmızı-Siyah Ağaç’lara götürsün? Alıştırma 9.7. Neden Kırmızı-Siyah Ağaç uygulamasında sil(x) yöntemi u.parent = w.parent belirlemesini gerçekleştirir? Zaten bunun splice(w) için yapılan çağrı tarafından yapılması gerekmiyor muydu? Alıştırma 9.8. Bir 2-4 Ağaç’ının, T, nl yaprağı ve ni iç düğümü olduğunu düşünün. 1. ni minimum değeri nl cinsinden bir fonksiyonu olarak nedir? 2. ni maksimum değeri nl cinsinden bir fonksiyonu olarak nedir? 3. T’ ağacı T kırmızı-siyah ağacını temsil ediyorsa, T’ kaç tane kırmızı düğüme sahiptir? 211 Alıştırma 9.9. En az n düğüm ve en çok 2 log n – 2 yüksekliğe sahip bir İkili Arama Ağaç’ı verilmiştir. Kırmızı ve siyah düğümlerini her zaman siyah-yükseklik ve kırmızı-olmayankenar özelliklerini sağlayacak şekilde renklendirmek mümkün müdür? Eğer öyleyse, soladayanım özelliğini karşılayacak şekilde de yapılabilir mi? Alıştırma 9.10. Varsayın ki, T1 ve T2 aynı siyah yüksekliğe, h, sahip iki Kırmızı-Siyah Ağaç olsun, ve öyle ki T1 içindeki en büyük değer T2 içindeki en küçük değerden daha küçük olsun. T1 ve T2, tek kırmızı-siyah ağacı içine O(h) zamanda nasıl birleştirilir? Alıştırma 9.11. Alıştırma 9.10 için hazırladığınız çözümü, farklı siyah yüksekliğe, h1 ≠ h2, sahip iki Kırmızı-Siyah Ağaç için tekrar düşünün. Çalışma zamanı O(max (h1, h2)) olmalıdır. Alıştırma 9.12. ekle(x) işlemi sırasında AVL Ağaç’ı (çoğu iki döndürüş içeren) en fazla bir yeniden-dengeleme işlemi gerçekleştirmelidir. (Bkz. Şekil 9.10). Bir AVL Ağacı ve bu ağaç üzerinde yeniden-dengelenme işlemlerinin log n zamanda çalıştığı bir sil(x) işlemi örneği verin. Alıştırma 9.13. Yukarıda açıklandığı gibi AVL Ağaç’larını gerçekleştiren bir AVLAğaç sınıfı uygulayın. Performansını Kırmızı-Siyah Ağaç’ın performansı ile karşılaştırın. Hangi uygulamanın bul(x) işlemi daha hızlıdır? Alıştırma 9.14. Sekme Listesi Sıralı Küme, Günah Keçisi Ağacı, Treap , ve Kırmızı-Siyah Ağaç veri yapılarının Sıralı Küme uygulamalarında gerçekleştirilen bul(x), ekle(x), sil(x) işlemlerinin performanslarını göreceli olarak karşılaştıran bir dizi deney tasarlayın ve uygulayın. Veriler ve silme sıraları rasgele ve sıralı olan birden fazla test senaryosu hazırlayın. 212 Bölüm 10 Yığınlar Bu bölümde, Öncelik Kuyruğu veri yapısının son derece yararlı iki uygulaması tanıtılıyor. Bu yapıların her ikisinde de Yığın adında İkili Ağaç’ın "Dağınık Yığın" anlamına gelen özel bir türü incelemeye alınmıştır. İkili Arama Ağaç’larının tersi olarak, İkili Ağaç’lara esneklik kazandırılmıştır. Aynı verinin geliş sırasına göre farklı düğüm lokasyonlarında depolanmaması analizimizi kolaylaştırır. Yığın uygulamasının birincisi bir dizi kullanır. Çok hızlı uygulama olarak bilinen en hızlı sıralama algoritmalarından biri yığında sıralamaktır. (Bkz. Bölüm 11.1.3). İkinci uygulamada Öncelik Kuyruğu’nun önceliğinden yararlanarak bir meld(d) işlemi ikinci bir öncelik kuyruğunun elemanları birleştirmekte kullanılmıştır. 10.1 İkili Yığın: Bir Örtülü İkili Ağaç Öncelik Kuyruğu dört yüz yıldan daha eski bir tekniğe dayanır. Eytzinger yöntemi bir dizi kullanarak enine-aramayla İkili Ağaç’ı gerçekleştirir (Bölüm 6.1.2). Bu şekilde, kök, 0 konumunda depolanır. kökün sol çocuğu 1 konumunda saklanır, 2 pozisyonunda kökün sağ çocuğu, ve kökün sol çocuğun sol alt böylece 3 de depolanır (Bkz.Şekil 10.1). Yeterince büyük bir ağaca Eytzinger yöntemini uygularsanız, bazı desenler ortaya çıkar. i’inci endeksteki düğümün sol alt çocuğunda left(i) = 2i + 1 yer almaktadır ve i’inci endeksteki düğümün sağ alt çocuğunda right(i) = 2i + 2 yer almaktadır. i endeksindeki düğümün menşei parent(i) = (i – 1 )/2. 213 İkili Yığın elemanlarını yığın-sıralı yapan bir İkili Ağaç’ın temsilinde kullanılan teknik açıklanırken başlangıç noktası i = 0, kök değeri dışında herhangi bir i çocuğu için saklanan değerin, , parent(i) menşei’de depolanmış değerden daha küçük olmaması olarak belirtilmiştir. Bunu takiben, Öncelik Kuyruğu'ndaki en küçük değerin 0 pozisyonda (kök) saklandığını çıkarsaymak kolaydır. n elemanlı İkili Yığın’ın elemanlarını a dizisinde depolayabiliriz: ekle(x) işlemi uygulaması kolaydır. Tüm dizi tabanlı yapılarında olduğu gibi dolu olduğunu kontrol ederiz (a.length = n edilerek), ve eğer öyleyse, a bir eleman büyütülür. Sonra, bir 214 a[n] konumuna x yerleştirmek için, n bir artırılır. Bu noktada, geriye tüm bu yığın özelliğini korumayı sağlamak kalır. x, ebeveyninden daha küçük olana kadar defalarca x’i ebeveyni ile yer değiştirerek bunu yapabiliriz. (Bkz. Şekil 10.2) Yığının en küçük değerini silen remove() işlemi uygulaması biraz yanıltıcıdır. Küçük değerin kökte olduğu biliniyor, ancak onu sildikten sonra yığın özelliğini korumayı sağlamak için eleman diğer değerlerle tekrar yığına kazandırılmalıdır. Bunu yapmak için en kolay yol, a[n - 1] değerini kök ile yer değiştirmektir, o değer silinir ve n bir azaltılır. Ne yazık ki, kökteki yeni eleman şimdi en küçük eleman olmaz, bu nedenle doğru yeri bulmak için elemanın sıralamada alt kademede bulunan iki çocuğu ile defalarca karşılaştırılması uygun olacaktır. Karşılaştırma sonucunda eleman üçünün en küçüğü ise, silme işlemi sona erer. Aksi takdirde, onun iki çocuğu ile bu eleman alt kademelere doğru yer değiştirir. Diğer dizi-bazlı yapılar, Önerme 2.1’deki amortize argümana dayanarak yeniden_oluştur(u) işlemi için yapılan çağrıların çalışma zamanını dikkate almamıştı. ekle(x) ve sil(x) işlemleri için yapılan çağrıların çalışma zamanı (içsel) İkili Ağaç’ın yüksekliğine bağlıdır. Tam İkili 215 Ağaç’ta son seviye dışında her seviye olası maksimum depolama kapasitesindedir. Bu nedenle, ağacın yüksekliği h ise, o zaman en az 2h düğümü vardır. Matematiksel anlamda bu, Bu denklemin her iki tarafının logaritması alındığı takdirde, Bu nedenle, ekle(x) ve sil(x) işleminin çalışma zamanı O(log n) olur. 216 217 10.1.1 Özet Teorem 10.1. Aşağıdaki teorem İkili Yığın performansını özetler: Bir İkili Yığın, Öncelik Kuyruğu arayüzünü uygular. yeniden_oluştur(u) işlemi için yapılan çağrıların maliyeti dikkate alınmayarak, ekle (x), sil (x) ve bul(x) işlemleri İkili Yığın tarafından işlem başına O(log n) zamanda desteklenir. Ayrıca, boş bir İkili Yığın ile başlandığında, ekle (x) ve sil (x) işlemleri toplam m defa çağrılırsa, tüm yeniden_oluştur (u) çağrıları için gerekli toplam çalışma süresi O(m) olur. 10.2 Karışık Yığın: Rasgele Karışık Yığın Bu bölümde, yığın-sıralı İkili Ağaç yapısı üzerine gerçekleştirilen Öncelik Kuyruğu’nun bir uygulaması olan Karışık Yığın tanıtılıyor. Bununla birlikte, İkili Ağaç’tan farklı olarak İkili Ağaç tamamen elemanların sayısı ile tanımlandığı halde, İkili Ağaç’taki düğümlerin lokasyonu üzerinde herhangi bir kısıtlama yoktur. Karışık Yığın için ekle(x) ve sil() işlemleri uygulanırken merge(h1, h2) işleminden faydalanılmıştır. İşlem iki ayrı yığın düğümünü, h1, h2, birleştirmek üzere alır. h1 kökenli altağacın tüm elemanlarını ve h2 kökenli altağacın tüm elemanlarını yığının kökü olan düğümle birlikte döndürür. merge (h1, h2) işleminin özyinelemeli olarak tanımlanmış olması programcıya birtakım kolaylık sağlar. (Bkz. 10.4) Boş küme ile birleştirme durumunda, h1 veya h2, biri yada ikisi, nil olmalıdır. Bu durumda işlem sırasıyla, h2 veya h1 döndürür. Aksi takdirde, varsayalım h1.x ≤ h2.x. Kök h1.x olduğu takdirde, h2, özyinelemeli olarak h1.left veya h1.right ile birleştirilmelidir. h2’yi birleştirmek için h1.left mi? h1.right mi? sorusunun cevabına karar vermek için rasgelelik kullanılır ve yazı tura atılır. 218 h1.x > h2.x koşulu geçerliyse, ekstra kod yazmaya gerek yoktur. Aynı kodu küçük değişikliklerle, h1 ve h2 rollerini tersine çevirerek rahatlıkla işletebilirsiniz. 219 h1 ve h2 elemanlarının toplam sayısına n dersek merge (h1, h2) işleminin beklenen çalışma zamanı O(log n) olur. Bir sonraki bölüm bunun analizini gösteriyor. merge (h1, h2) işleminden faydalandığımız takdirde, ekle(x) işlemini uygulamak kolaydır. x değerini içeren yeni bir u düğümü kök kabul ederek oluştururuz. Sonra yığını kök u ile birleştiririz. Beklenen çalışma zamanı O(log n) = O(log (n+1)) olur. remove() işlemi fonksiyonu benzerdir ve uygulaması kolaydır. Silmek istediğimiz düğümü kök kabul edersek, kökün iki çocuğunu birleştiririz ve ortaya çıkan yığın istediğimiz sonucu bize verir. Beklenen çalışma zamanı O(log n) olur. Karışık Yığın, beklenen çalışma zamanı O(log n) olan başka işlemleri de uygulamamıza izin veriyor: 220 • sil(u) : u • em(h) : Karışık Yığın, h, içindeki düğümü yığından silinir (u.x anahtarını da siler). tüm elemanlar bu yığına eklenir, h elemanları silinerek nil olur. Bu işlemlerin her biri için merge(h1,h2) işlemi sabit sayıda tekrar tekrar çalıştırılırsa, beklenen toplam çalışma zamanı her bir merge için O(log n) olarak hesaplanmalıdır. 10.2.1 merge(h1, h2) Analizi merge(h1, h2) analizinde İkili Ağaç üzerinde rasgele yürüyüş tekniği kullanılıyor. İkili Ağaç rasgele yürüyüşü ağacın kökünden başlar. Rasgele yürüyüşün, her aşamasında, yazı tura atılır ve sonucuna bakarak bulunduğumuz düğümün sol veya sağ çocuğuna doğru ilerlemeye karar veririz. Ağacın sonuna geldiğimizde (bulunduğumuz düğüm nil ise) yürüyüş biter. Aşağıdaki önermenin dikkat çekici yönü, İkili Ağaç’ta depolanan değerlerin düğümlerin lokasyonuna bağlı olmamasıdır. Önerme 10.1. n düğümü olan İkili Ağaç’a uygulanan rasgele yürüyüşün beklenen uzunluğu en fazla log(n+1) olur. Kanıt. n üzerinde tümevarım yöntemiyle kanıtlanacak. Taban durumunda, n = 0, ve yürüyüş uzunluğu 0 = log n + 1 kabul edilir. Sonucun tüm negatif olmayan tamsayılar n’ < n için doğru olduğunu varsayalım. Kökün sol alt ağacının büyüklüğünün n1 olduğunu gösterelim, böylece n2 = n – n1 – 1 kökün sağ altağacın boyutudur. Yürüyüş kökten başlar, bir adım sonra n1 veya n2 boyutlarında olan altağaç ile devam eder. Tümevarım hipotezimiz yürüyüşün beklenen uzunluğunun 221 olduğunu bize söylüyor, çünkü n1 ve n2, her ikisi de n’den küçüktür. log fonksiyonu içbükey özelliğe sahip olduğundan n1 = n2 = (n - 1) / 2 olduğunda, E[W] en büyük olur. Bu nedenle, rasgele yürüyüş tarafından atılan adımların beklenen sayısı olur. Konu dışında kalsa da okuyucuya belirtmemiz gereken bir açılım da Önerme 10.1’nin kanıtını entropi yöntemi ile yapmaktır. Önerme 10.1 için Bilgi Teorik Kanıt. i'inci dış düğümün derinliği di ile gösterilsin. n düğümlü İkili Ağaç’ın n+1 dış düğüme sahip olduğunu hatırlayın. i’inci dış düğüme ulaşan rasgele yürüyüşün olasılığı tam olarak, olduğu için rasgele yürüyüşün beklenen uzunluğu ile verilir. Bu denklemin sağ tarafı n+1 eleman üzerinde bir olasılık dağılımının entropisi olarak kolaylıkla tanınabilir. n+1 eleman için entropik değer log(n+1) aşmaz, bu kanıtı tamamlar. Rasgele yürüyüşler bu sonuçla birlikte, merge(h1, h2) işleminin çalışma zamanının O(log n) olduğunu kanıtlamaya da yarar. Önerme 10.2. n1 ve n2 düğümlü içeren iki yığının kökleri sırasıyla h1 ve h2 ise, o zaman merge(h1, h2) için beklenen çalışma süresi, n = n1 + n2 iken, O(log n) olur. 222 Kanıt. h1 yada h2 kökenli yığınlar için merge algoritmasının her adımında, rasgele yürüyüş uygulanır. Bu iki rasgele yürüyüş bittiğinde (h1 = null yada h2 = null) algoritma sona erer. Bu nedenle, merge algoritması tarafından gerçekleştirilen adımların beklenen sayısı en fazla olur. 10.2.2. Özet Aşağıdaki teorem Karışık Yığın performansını özetler: Teorem 10.2. Karışık Yığın, Öncelik Kuyruğu arayüzünü uygular. ekle (x), sil () işlemleri Karışık Yığın tarafından işlem başına O(log n) beklenen zamanda desteklenir. 10.5 Tartışma ve Alıştırmalar İkili Ağaç’ın bir dizi yada liste olarak içsel gösterimi ilk olarak Eytzinger [27] tarafından önerilmiştir. Kendisi asil ailelerin soy ağaçlarını içeren kitaplarda bu temsili kullanmıştır. Burada anlatılan İkili Yığın veri yapısı ilk olarak Williams [78] tarafından tanıtıldı. Burada açıklanan Rasgele Karışık Yığın veri yapısı ilk olarak Gambin ve Malinowski [34] tarafından önerilmiştir. Diğer Karışık Yığın uygulamaları vardır: Hiçbiri Karışık Yığın yapısı kadar basit olmamasına rağmen, sol-odaklı yığınlar [16, 48, Bölüm 5.3.2], binom yığınları [75], Fibonacci yığınları [30], eşleştirme yığınları [29], ve çarpık yığınlar [72]. Yukarıdaki yapıların bazıları decreaseKey(u, y) işlemini destekler. Bu işlem u düğümünde saklanan değeri y olarak düşürerek belirler (y ≤ x önkoşuldur). Yukarıdaki yapıların çoğunda, u düğümünü silerek ve yeni bir y değerini ekleyerek bu işlem O(log n) zamanda desteklenir. Bununla birlikte, bu yapıların bir kısmı decreaseKey (u, y) işlemini daha etkili uygulayabilir. Özellikle, decreaseKey(u, y) Fibonacci yığınları için O(1) amortize zaman ve eşleştirme yığınlarının [25] özel bir sürümü için O(log log n) amortize zaman alır. Bu daha verimli 223 decreaseKey(u, y) işleminin, Dijkstra'nın en kısa yol algoritması [30] dahil, grafik algoritmalarını hızlandıran uygulamaları bulunmaktadır. Alıştırma 10.1. Şekil 10.2 sonunda gösterilen İkili Ağaç’a 7 ve daha sonra 3 değerlerinin eklenmesini gösterin. Alıştırma 10.2. Şekil 10.2 sonunda gösterilen İkili Ağaç’tan sonraki iki değerin (6 ve 8) silinmesini gösterin. Alıştırma 10.3. İkili Ağaç’ta depolanan bir a[i] değerini silen sil(i) yöntemini uygulayın. Bu yöntemin çalışma zamanı O(log n) olmalıdır. Bu yöntemin yararlı olmasının neden olası olmadığını açıklayın. Alıştırma 10.4. Bir İkili Ağaç’ın genellemesi olan d-Ağaç’ında her iç düğümün d çocuğu vardır. Tam d-li Ağaç’lar, dizileri kullanarak Eytzinger yönteminin kullanılmasıyla gösterilebilir. Bir i endeksi verildiğinde i'nin menşei endeksini ve i'nin d çocuğunu d-li Ağaç gösteriminde belirleyecek denklemler bulmaya çalışın. Alıştırma 10.5. Alıştırma 10.4’te öğrendiklerinizi kullanarak, İkili Yığın’ın d-li genelleştirmesi olan D-li Yığın tasarlayın ve uygulayın. D-li Yığın üzerindeki işlemlerin çalışma zamanlarını analiz edin ve D-li Yığın uygulamasının performansını burada verilen İkili Yığın uygulamasının performansına karşı test edin. Alıştırma 10.6. Şekil 10.4’te gösterilen, h1, Karışık Yığın’ına 17 ve daha sonra 82 değerlerinin eklenmesini gösterin. Gerektiğinde rasgele biti örneklemek için madeni para kullanın. Alıştırma 10.7. Şekil 10.4’te gösterilen, h1, Karışık Yığın’ından sonraki iki değerin (4 ve 8) silinmesini gösterin. Gerektiğinde rasgele biti örneklemek için madeni para kullanın. Alıştırma 10.8. Bir Karışık Yığın’dan u düğümünü kaldıran sil(u) yöntemini uygulamak. Bu yöntemin beklenen çalışma zamanı O(log n) olmalıdır. 224 Alıştırma 10.9. İkili Yığın veya Karışık Yığın içinde depolanan ikinci en küçük değerin sabit zamanda nasıl bulunacağını gösterin. Alıştırma 10.10. İkili Yığın veya Karışık Yığın içinde depolanan k’nci en küçük değerin O(k log k) zamanda nasıl bulunacağını gösterin (İpucu: İkinci bir yığın yardımcı olarak kullanılabilir). Alıştırma 10.11. Varsayın ki, toplam uzunluğu n olan, k adet sınıflandırma-listesi verilmiştir. Bir yığın kullanarak bunları O(n log k) zamanda sadece tek adet sıralı liste içine nasıl yerleştireceğimizi gösterin (İpucu: k = 2 iken başlamak öğretici olabilir.) 225 Bölüm 11 Sıralama Algoritmaları Bu bölümde, n elemanlı bir kümenin sıralaması ile ilgili algoritmalar tanıtılıyor. Bu veri yapıları üzerine bir kitap için garip bir konu gibi görünebilir, ama buraya dahil edilmesi için birçok iyi nedeni vardır. En belirgin nedeni bu sıralama algoritmalarının ikisi (hızlı-sıralama ve dikey-sıralama) daha önce detaylarıyla incelediğimiz veri yapılarının ikisi ile ilişkilidir (sırasıyla, rasgele ikili arama ağaçları ve yığınlar). Bu bölümün ilk kısmı yalnızca karşılaştırmalara dayanan sıralama algoritmalarını tartışıyor ve O(n log n) zamanda çalışan üç algoritma sunuyor. Her üç algoritmanın da asimptotik en iyi olduğu ortaya çıkmıştır; sadece karşılaştırma kullanan hiçbir algoritma en kötü veya ortalama durumda bile kabaca n log n karşılaştırmadan daha azını yapmanın önüne geçememiştir. Devam etmeden önce, dikkat etmeliyiz ki, önceki bölümlerde sunulan Sıralı Küme veya Öncelik Kuyruğu uygulamalarının herhangi biri O(n log n) zamanda çalışan bir sıralama algoritması elde etmek için kullanılabilir. Örneğin, bir İkili Yığın veya Karışık Yığın üzerinde n ekle(x) işlemini takip eden n sil() işlemini gerçekleştirerek n elemanı sıralayabiliriz. Alternatif olarak, İkili Arama Ağacı veri yapılarının herhangi biri üzerinde n ekle(x) işlemini kullanır ve daha sonra elemanları sıralı-halde elde etmek için bir içsel-sıra gezintisi (Alıştırma 6.8) gerçekleştirebiliriz. Ancak her iki durumda da, hiçbir zaman tam olarak kullanılmayan bir yapı oluşturmak için bir sürü yüke katlanmak zorunda kalınmaktadır. Bu nedenle sıralama, mümkün olan en hızlı, basit ve alan-verimli direkt yöntemler geliştirmemizi gerekli kılan çok önemli bir sorun olarak ortaya çıkmaktadır. Bu bölümün ikinci kısmı, karşılaştırmalar dışında kalan tüm diğer işlemlerin bahis dışı kaldığını gösteriyor. Nitekim, dizi endekslemesini kullanarak, {0, .., nc – 1} aralığında n tamsayı kümesini O(cn) zamanda sıralamak mümkündür. 226 11.1 Karşılaştırmaya-Dayalı Sıralamalar Bu bölümde, üç sıralama algoritmasını tanıtılıyor: birleştirerek-sıralama, hızlı-sıralama ve dikey-sıralama. Bu algoritmaların her biri girdi olarak a dizisini alır ve elemanlarını artansırada O(n log n) (beklenen) zamanda sıralar. Bu algoritmaların hepsi karşılaştırmayadayalıdır. İkinci argüman, c, compare(a, b) yöntemini uygulayan bir Karşılaştırıcı’dır. Bu algoritmalar hangi tür veriyi sıraladığını önemsemez; veri üzerinde yaptıkları tek işlem compare(a, b) yöntemini kullanarak yapılan karşılaştırmadır. Bölüm 1.2.4’ten hatırlamalısınız ki, a < b ise compare(a, b) negatif bir değer döndürür, a > b için pozitif bir değer döndürür, ve a = b ise sıfır döndürür. 11.1.1 Birleştirerek-Sıralama Birleştirerek-sıralama algoritması klasik bir özyinelemeli böl ve yönet örneğidir: a uzunluğu en fazla 1 ise, o zaman a zaten sıralanmıştır, bu yüzden hiçbir şey yapmayız. Aksi takdirde, a iki parçaya bölünür, a0 = a[0], ..., a[n/2 – 1] ve a1 = a[n/2], ..., a[n – 1]. Özyinelemeli olarak a0 ve a1 sıralanır, ve sonra (şimdi sıralı olan) a0 ve a1 dizileri tam sıralı bir a dizisini oluşturacak şekilde birleştirilir. Bir örnek Şekil 11.1 'de gösterilmektedir. 227 İki sıralı diziyi, a0 ve a1, birleştirmek, sıralamaya göre oldukça kolaydır. Her seferinde bir elemanı a’ya ekleriz. a0 veya a1 boşsa, o zaman diğer (boş olmayan) dizideki sonraki elemanları ekleriz. Aksi takdirde, a0 içindeki sonraki elemanın ve a1 içindeki sonraki elemanın en küçüğünü alır ve bunu a dizisine ekleriz: Dikkat etmelisiniz ki, merge(a0, a1, a, c) algoritması a0 veya a1 dizilerindeki elemanları tüketmeden önce en fazla n – 1 karşılaştırma yapar. 228 Birleştirerek-sıralama için gerekli çalışma zamanını anlamak için en kolay yol, özyineleme ağacını düşünmek olacaktır. Şimdilik n’in ikinin üssü olduğunu varsayın, öyle ki log n bir tamsayı iken n = 2log n olsun. Şekil 11.2’e bakın. Birleştirerek-sıralama n elemanı sıralama problemini, her biri n/2 elemanı sıralayan iki problem haline dönüştürür. Bu iki altproblem yine daha sonra iki problem haline dönüşür, her biri n/4 elemandan oluşan dört altproblem elde edilir. Bu dört altproblem her biri n/8 elemandan oluşan sekiz altproblem haline dönüşür, ve benzeri. Bu işlemin en son alt kısmında, her birinin boyutu 2 olan n/2 alt problem, her birinin boyutu 1 olan n probleme dönüştürülür. Her n/2i boyutunda altproblem için veri birleştirme ve kopyalama için harcanan zaman O(n/2i) olur. n/2i boyutunda 2i altproblem olduğu için 2i boyutunda problemler üzerinde çalışılan toplam zaman, özyinelemeli çağrılar sayılmaksızın, olur. Bu nedenle, birleştirerek-sıralama için harcanan toplam zaman olur. Aşağıdaki teoremin kanıtı önceki analize dayanmaktadır, ama n için 2’in üssü değil iken biraz daha dikkatli olunmak zorundadır. 229 Teorem 11.1. mergeSort(a, c) algoritması O(n log n) zamanda çalışır ve en çok n log n karşılaştırma gerçekleştirir. Kanıt. n üzerine tümevarım yöntemiyle kanıtlanacak. Temel durum, n = 1, önemsiz kabul ediliyor. Uzunluğu 0 ya da 1 olan bir dizi sunulduğunda, algoritma herhangi bir karşılaştırma yapmadan sadece diziyi döndürür. Toplam uzunluğu n olan iki sıralı listeyi birleştirmek en fazla n – 1 karşılaştırma yapmayı gerektirir. n uzunluğunda bir a dizisi üzerinde mergeSort(a, c) tarafından gerçekleştirilen karşılaştırmaların maksimum sayısı C(n) ile belirtilsin. n çift ise, o zaman iki altproblem için tümevarım hipotezini uygulayarak, elde ederiz. n tek olduğu zaman durum biraz daha karmaşıktır. Bu durumda, doğrulanması kolay iki eşitsizlik kullanıyoruz: her x ≥ 1 için ve her x ≥ ½ için. (11.1) eşitsizliği log(x) + 1 = log(2x) olması gerçeğinden ileri geliyor. (11.2) eşitsizliği log fonksiyonunun içbükey özelliğe sahip olması gerçeğinden ileri geliyor. Elimizdeki bu araçlar ile, tek sayıdaki n için, 230 olur. 11.1.2 Hızlı-Sıralama Hızlı-sıralama algoritması diğer bir klasik böl ve yönet algoritmasıdır. İki altproblemi çözdükten sonra birleştiren birleştirerek-sıralamanın aksine hızlı-sıralama, çalışmalarının hepsini açık ve belirgin bir şekilde yapar. Hızlı-sıralamanın tanımlanması oldukça kolaydır: a içinden rasgele bir pivot elemanı, x, seçilir; x’den küçük elemanların oluşturduğu küme, x’e eşit elemanların oluşturduğu küme, ve x’den büyük elemanların oluşturduğu küme olmak üzere a üç parçaya bölünür, ve nihayet, özyinelemeli olarak birinci ve üçüncü kümeler sıralanır. Bir örnek Şekil 11.3 'de gösterilmiştir. Tüm bunlar yerli yerinde yapılır, böylece sıralanacak altdizilerin kopyalarını yapmak yerine, quickSort(a, i, n, c) yöntemi sadece a[i], …, a[i + n – 1] altdizisini sıralar. Başlangıçta, bu yöntem quickSort(a, 0, a.length, c) argümanları ile çağrılır. Hızlı-sıralama algoritmasının kalbinde yerinde bölümleme algoritması yatar. Bu algoritma, herhangi bir ekstra alan kullanmadan, a elemanlarını yer değiştirerek p ve q endekslerini hesaplar; öyle ki 231 Kodda while döngüsü tarafından yapılan bu bölümleme, ilk ve son koşulu korumak şartıyla p’nin tekrarlanarak artırılmasıyla ve q’nun azaltılmasıyla çalışır. Her adımda, j pozisyonundaki eleman ya öne taşınır, ya olduğu yerde sola kaydırılır, yada geriye taşınır. İlk iki durumda j artırılırken, son durumda iken j artırılmaz, çünkü j pozisyonundaki yeni eleman henüz işlenmemiştir. 232 Hızlı-sıralama Bölüm 7.1’de anlatılan Rasgele İkili Arama Ağaç’ları ile çok yakından ilgilidir. Aslında, hızlı-sıralamaya verilen girdi n birbirinden farklı elemandan oluşmuş ise, o zaman hızlı-sıralamanın özyineleme ağacı bir Rasgele İkili Arama Ağacı’dır. Bunu görmek için, hatırlamanız gerekiyor ki, bir Rasgele İkili Arama Ağacı’nı oluşturacağınız zaman yaptığımız ilk şey, rasgele bir x elemanını seçmek ve bunu ağacın kökü yapmaktı. Bundan sonra, her bir eleman sonunda x ile karşılaştırılmıştı, küçük elemanlar sol altağacın içine giderken, daha büyük elemanlar sağa doğru gitmişti. Hızlı-sıralamada rasgele bir x elemanı seçeriz ve hemen her şeyi x ile karşılaştırırız, daha küçük elemanları dizinin başına koyarız, ve daha büyük elemanları dizinin sonuna koyarız. Rasgele İkili Arama Ağacı özyinelemeli olarak kökün sol altağacına daha küçük elemanları, ve kökün sağ altağacına daha büyük elemanları eklerken, hızlı-sıralama özyinelemeli olarak dizinin başlangıcını ve dizinin sonunu sıralar. Rasgele İkili Arama Ağaç’lar ve hızlı-sıralama arasındaki yukarıdaki benzerlikler, Önerme 7.1’i kullanarak hızlı-sıralama hakkında bir bildiri yapabileceğimiz anlamına gelir. Önerme 11.1. Hızlı-sıralama 0, …, n – 1 içindeki tamsayıları içeren bir diziyi sıralamak için çağrıldığında, i elemanının pivot eleman ile karşılaştırılmasının beklenen sayısı en fazla Hi+1 + Hn-i olur. Harmonik sayıların küçük bir özetlemesi bize hızlı-sıralamanın çalışma zamanı ile ilgili aşağıdaki teoremi verir: Teorem 11.2. Hızlı-sıralama n birbirinden farklı elemanı içeren bir diziyi sıralamak için çağrıldığında, yapılan karşılaştırmaların beklenen sayısı en fazla 2n ln n + O(n) olur. Kanıt. n birbirinden farklı eleman sıralanırken hızlı-sıralama tarafından yapılan karşılaştırmaların sayısı T olsun. Önerme 11.1 ve beklentinin doğrusallık özelliği kullanılarak 233 elde edilir. Teorem 11.3 sıralanan tüm elemanların birbirinden farklı olduğu durumu anlatıyor. Girdi, a, dizisi, yinelenen elemanları içeriyorsa, hızlı-sıralamanın beklenen çalışma süresi daha kötü değildir ve hatta daha da iyi olabilir; her zaman yinelenen bir eleman, x, pivot olarak seçilir, x’in tüm oluşumları birbiriyle gruplanır, ve iki altproblemin hiçbirinde yer almazlar. Teorem 11.3. quickSort (a, c) yöntemi O(n log n) beklenen zamanda çalışır ve gerçekleştirdiği karşılaştırmaların beklenen sayısı en fazla 2n ln n + O(n) olur. 234 11.1.3 Yığın-Sıralama Yığın-sıralama algoritması diğer bir yerinde-sıralama algoritmasıdır. Hatırlayın ki, İkili Yığın veri yapısı tek dizi kullanarak bir yığını gösteriyordu. Yığın-sıralama algoritması girdi, a, dizisini bir yığına dönüştürür, ve daha sonra art arda minimum değeri ayıklar. Daha özel olarak, bir yığın, a, dizisi içindeki n elemanı en küçük değer kökte, a[0], saklanmak üzere, a[0], …, a[n – 1] lokasyonlarında depolar. a bir İkili Yığın’a dönüştürüldükten sonra, yığın sıralama algoritması tekrar tekrar a[0] ile a[n – 1]’i yer değiştirerek n’i bir azaltır, ve trickledown(0) çağrılır. Böylece a[0], ..., a[n - 2], bir kere daha geçerli bir yığını gösterir. Bu süreç sona erdiğinde (çünkü, n = 0), a elemanları azalan sırada depolanır, a sıralaması ters çevrilerek nihai sıralama kriteri elde edilmelidir.19 Şekil 11.4 örnek bir heapSort(a, c) çalıştırmasını gösteriyor. Yığın-sıralamada anahtar kurucu altprogram, sıralı olmayan bir a dizisini bir yığın haline dönüştürmelidir. Defalarca İkili Yığın, ekle(x) yöntemini çağırarak bunu O(n log n) zamanda yapmak kolay olurdu ama aşağıdan-yukarıya algoritma kullanarak daha iyi hale gelebilir. Hatırlayın ki, bir İkili Yığın içinde a[i]’nin çocukları a[2i + 1] ve a[2i + 2] konumlarında saklanır. her bir elemanlarının çocuğu yoktur anlamına gelir. Diğer bir deyişle, boyutu 1 olan bir alt yığındır. Şimdi, geriye doğru çalışırsak, her için trickleDown(i) çağrısı yapabiliriz. Bu çalışır, çünkü trickleDown(i) çağrısı yapıldığı zaman a[i]’ nin her iki çocuğu, bir altyığının köküdür, böylece trickleDown(i) çağrısı a[i]’yi kendi altyığınının kökü yapar. 19 Algoritma alternatif bir compare(x, y) fonksiyonunu yeniden tanımlayabilir, böylece yığın sıralama algoritması doğrudan artan sırada elemanları düzenleyebilir. 235 Aşağıdan-yukarıya strateji ile ilgili ilginç bir şey n defa ekle(x) çağırmaktan daha etkili olmasıdır. Bunun için fark etmelisiniz ki, n/2 eleman için, hiçbir iş yapmıyoruz, n/4 eleman için a[i] kökenli ve yüksekliği bir olan bir altyığın üzerine trickleDown(i) çağrısı yapıyoruz, n/8 eleman için yüksekliği iki olan bir altyığın üzerine trickleDown(i) çağrısı yapıyoruz, ve benzeri. trickleDown(i) tarafından yapılan çalışma bir a[i] kökenli altyığının yüksekliği ile orantılı olduğu için, bu yapılan toplam iş en fazla olur. Sondan ikinci eşitliği, toplamını, ilk defa yazı gelene kadar atılan yazı-turanın beklenen sayısına eşit olduğunu tanıyarak ve Önerme 4.2’yi uygulayarak oluşturduk. Aşağıdaki teorem heapSort(a, c) performansını açıklıyor. Teorem 11.4. heapSort(a, c) yöntemi O(n log n) zamanda çalışır ve en fazla 2n log n + O(n) karşılaştırma gerçekleştirir. Kanıt. Algoritma üç adımda çalışır: (1) a dizisinin bir yığın içine dönüştürülmesi (2) a içinden art arda minimum elemanın çıkarılması, (3) a elemanlarının ters çevrilmesi. Adım 1’in çalışması için, O(n) zaman gerektiği ve O(n) karşılaştırma gerçekleştirdiğini az önce tartışmıştık. Adım 3, O(n) zaman alır ve hiçbir karşılaştırma yapmaz. Adım 2, trickleDown(0) 236 için n çağrı gerçekleştirir. i'inci böyle bir çağrı n - i büyüklüğünde bir yığın üzerinde çalışır ve en fazla 2log (n - i) karşılaştırma gerçekleştirir. i üzerinden bunların toplanması, sonucuyla, üç adımın her birinde yapılan karşılaştırmaların sayısını ekleyerek kanıt tamamlanır. 11.1.4 Karşılaştırmaya Dayalı Sıralama için Alt-Sınır Şimdiye kadar gördüğümüz karşılaştırmaya-dayalı sıralama algoritmalarının her biri O(n log n) zamanda çalışıyordu. Artık daha hızlı algoritmaların var olduğu ilgimizi çekmelidir. Bu soruya verilecek kısa cevap hayırdan ibarettir. a elemanları sadece karşılaştırmaya dayalı işlemlere izin veriyorsa, o zaman hiçbir algoritma kabaca n log n karşılaştırma yapmayı önleyemez. Bunu kanıtlamak zor değildir, ama biraz sebat gerektirir. Sonuçta, gerçeğinden ileri geliyor (Bu gerçeğin kanıtlanmasını Alıştırma 11.11 soruyor). İlk olarak dikkatimizi n belirli bir sabit değeri için birleştirerek-sıralama ve yığın-sıralama gibi deterministik algoritmalara odaklayarak başlayacağız. Birbirinden farklı n elemanı sıralamak için böyle bir algoritmanın kullanıldığını varsayın. Alt sınırı kanıtlamanın yolunda anahtar rol oynayan şu gözlemdir ki, bir sabit, n, değerine sahip deterministik bir algoritma için karşılaştırılan ilk eleman çifti her zaman aynıdır. Örneğin, heapSort(a, c) içinde n çift ise trickleDown(i) a[n – 1] için yapılan ilk çağrıda i = n/2 – 1 ve ilk karşılaştırma da a[n/2 – 1] ve elemanları arasında gerçekleştirilmektedir. 237 Tüm girdi elemanları birbirinden farklı olduğu için, bu ilk karşılaştırmanın sadece iki olası sonucu vardır. Algoritma tarafından yapılan ikinci karşılaştırma birinci karşılaştırmanın sonucuna bağlı olabilir. Üçüncü karşılaştırma böylece ilk ikisinin sonuçlarına bağlı olabilir, ve benzeri. Bu nedenle, herhangi bir deterministik, karşılaştırmaya-dayalı sıralama algoritması köklü bir ikili karşılaştırma ağacı olarak görülebilir. Bu ağacın her bir, u, iç düğümü u.i ve u.j endeks çifti ile etiketlenmiştir. a[u.i] < a[u.j] ise algoritma sol alt ağaca doğru ilerler, aksi takdirde sağ alt ağaca doğru ilerler. Bu ağacın her, w, yaprağı 0, …, n – 1 aralığındaki bir w.p[0], …, w.p[n – 1] permütasyonu ile etiketlenmiştir. Bu permütasyon, karşılaştırma ağacı bu yaprağa ulaşırsa, a’yı sıralamak için gereklidir. Yani, Boyutu, n = 3 olan, bir dizi için karşılaştırma ağacının örneği Şekil 11.5’te gösterilmiştir. Bir sıralama algoritması için karşılaştırma ağacı bize algoritma hakkında her şeyi anlatır. Bu tam olarak birbirinden farklı, n, elemana sahip herhangi bir girdi dizisi, a, için yapılacak karşılaştırmaların birbiri ardı sıra dizilimlerini ve algoritmanın a’yı sıralamak için onu nasıl yeniden düzenleyeceğini söyler. Sonuç olarak, karşılaştırma ağacı en az n! yapraktan oluşmalıdır; eğer değilse, o zaman aynı yaprağa yol açan (giden) iki farklı permütasyon mevcuttur; bu nedenle algoritma bu permütasyonların en az birini doğru olarak sıralamaz. Örneğin, Şekil 11.6 'deki karşılaştırma ağacı, 4 < 3! = 6 yaprağa sahiptir. Bu ağacı incelersek, görüyoruz ki, iki girdi dizisi 3, 1, 2 ve 3, 2, 1 en sağdaki yaprağa götürür. 3, 1, 2 girdisinde yaprak doğru çıktı üretir a[1] = 1, a[2] = 2, a[0] = 3. Ancak, 3, 2, 1 girdisi üzerinde bu düğüm 238 hatalı çıktı üretir a[1] = 2, a[2] = 1, a[0] = 3. Bu tartışma karşılaştırmaya dayalı algoritmalar için birincil alt sınıra yol açar. Teorem 11.5. Herhangi bir deterministik karşılaştırmaya dayalı, A, sıralama algoritması ve herhangi bir n ≥ 1 tamsayısı için, n uzunluğunda öyle bir, a, girdi dizisi vardır ki, A’nın a dizisini sıralamak için gerçekleştirdiği karşılaştırma sayısı en az log(n!)= n logn - O(n) olur. Kanıt. Yukarıdaki tartışma ile, A tarafından tanımlanan karşılaştırma ağacı en az n! yaprak içermektedir. Kolay bir tümevarım kanıtı gösteriyor ki, k yapraklı herhangi bir ikili ağaç en az log k yüksekliğe sahiptir. Bu nedenle, A’nın karşılaştırma ağacında derinliği en az log(n!) olan bir w yaprağı vardır ve bu yaprağa yol açan (giden) bir girdi, a, dizisi vardır. Girdi, a, dizisi üzerinde A en az log(n!) karşılaştırma gerçekleştirir. Teoremi 11.5 birleştirerek-sıralama ve yığın-sıralama gibi deterministik algoritmalar içindir, ama hızlı-sıralama gibi randomize algoritmaları hakkında bize hiçbir şey söylemez. Acaba karşılaştırma sayısına gelince bir randomize algoritma log(n!) alt sınırını aşabilir mi? Cevap, yine hayır. Yine, bunu kanıtlamanın yolu randomize algoritmaların ne olduğu hakkında farklı düşünmektir. Aşağıdaki tartışmada, varsayalım ki karar ağaçlarımızı "temizlemek” için şu yol takip ediliyor: Bazı girdi, a, dizisi tarafından ulaşılamayan herhangi bir düğüm silinmiştir. Bu temizlik sonrasında ağaç tam olarak n! yaprağa sahip olur. n! yaprağı vardır çünkü aksi takdirde, bu, doğru sıralamaya götürmezdi. En fazla n! yaprağı vardır, çünkü olası her farklı n elemanın n! permütasyonu, kökten başlayarak karar ağacının yapraklarına götüren tam bir yol oluşturur. 239 İki girdi alan deterministik, R, randomize sıralama algoritması düşünebiliriz. Sıralanması gereken bir girdi dizisi, a, ve [0, 1] aralığında rasgele reel sayılardan oluşan uzun bir dizi b = b1, b2, b3, …, bm. Rasgele sayılar algoritma için randomizasyon sağlıyor. Algoritma yazı tura atmak ya da rasgele bir seçim yapmak istediğinde, bunu b içinden bazı elemanı kullanarak yapar. Örneğin, hızlı-sıralamanın ilk pivot endeksini hesaplamak için algoritma [nb1] formülünü kullanabilir. Şimdi farkına varmalısınız ki, eğer b bazı özel dizilimi tarafından sabitlenebilirse, ardından ilişkilendirilmiş karşılaştırma, ağacına, algoritması, , sahip olan R deterministik bir sıralama , olur. Sonra, fark etmelisiniz ki {1, …, n} aralığında bir rasgele permütasyon seçtiğimizde, o zaman bu, ’nin içinden rasgele bir, w, yaprağını seçmek ile eşdeğerdir. Alıştırma 11.13, k yapraklı herhangi bir ikili ağaçtan seçilen rasgele bir yaprağın beklenen derinliğinin en az log k olduğunun kanıtlanmasını soruyor. Bu nedenle, (deterministik) algoritma, tarafından yapılan karşılaştırmaların beklenen sayısı {1, …, n} rasgele bir permütasyon içeren bir girdi dizisi verildiğinde en az log(n!) olur. Son olarak, ’nin her seçimi için bu doğrudur, bu nedenle R için de geçerlidir. Bu randomize algoritmalar için alt sınır kanıtını tamamlar. Teorem 11.6. Herhangi bir tam sayı n ≥ 1 için ve herhangi bir (deterministik veya randomize) karşılaştırmaya-dayalı sıralama algoritması, A, tarafından {1, …, n} aralığında rasgele permütasyon sıralaması yaparken gerçekleştirilen karşılaştırmaların beklenen sayısı en az log(n!) = n log n – O(n) olur. 11.2 Sayma Sıralama ve Taban Sıralaması Bu bölümde karşılaştırmaya dayalı olmayan iki sıralama algoritmasını çalışacağız. Küçük tamsayıları sıralamak için özelleştirilmiş olan bu algoritmalar, a elemanlarının (bir bölümünü) bir dizinin endeksleri gibi kullanarak, Teorem 11.5’in alt sınırlarını aşarlar. 240 şeklinde bir ifade düşünün. Bu ifade sabit zamanda çalışır, ancak, a[i] değerine bağlı olarak değişen c.length kadar farklı olası sonucu vardır. Bunun anlamı, bu tür bir komut içeren bir algoritmanın çalışması ikili ağaç olarak modellenemez. Sonuç olarak, bu bölümdeki algoritmaların karşılaştırmaya dayalı algoritmalardan daha hızlı sıralamalarını mümkün kılacak olan neden de budur. 11.2.1 Sayma Sıralaması Her biri 0, …, k – 1 aralığında n tamsayıdan oluşan bir girdi, a, dizisinin olduğunu varsayalım. Sayma-sıralama algoritması yardımcı bir c sayaç dizisi kullanarak, a’yı sıralar. Yardımcı bir b dizisi olarak a’nın sıralı versiyonunu çıktı olarak üretir. Sayma-sıralaması arkasındaki fikir basittir: Her i ∈ {0, …, k – 1} için, a içindeki i’nin yinelenme sayısını sayın ve bunu c[i]’de saklayın. Şimdi sıralama sonrasında, 0’ın yinelenme sayısını c[0], 1’in yinelenme sayısını c[1], 2’nin yinelenme sayısını c[2], …, k – 1’in yinelenme sayısını c[k – 1] tutacaktır. Bu yapan kod çok alımlıdır, ve çalıştırması Şekil 11.7 'de gösterilmiştir: 241 Bu koddaki ilk for döngüsü her c[i] sayacını, a içindeki i yinelemelerini sayarak, ayarlar. a değerlerini endeks olarak kullanarak, bu sayaçların tümü O(n) zamanda tek bir for döngüsü içinde hesaplanabilir. Bu noktada, çıkış dizisi b’yi doldurmak için doğrudan c’yi kullanabiliriz. Ancak, a elemanlarının ilişkili verileri varsa, bu işe yaramaz. Bu nedenle b içine a elemanlarını kopyalamak için biraz fazladan çaba harcamak gerekir. O(k) zaman alan bir sonraki for döngüsü sayaçların toplamını hesaplar, böylece i'den daha az ya da eşit olan elemanların sayısı c[i]’de saklanır. Özellikle, her i ∈ {0, …, k – 1} için, çıktı dizisi b’nin değerleri olur. Son olarak, algoritma, kendi elemanlarını çıktı b dizisine sıralı olarak yerleştirmek için bir geri tarama başlatır. Tararken, a[i] = j elemanı b[c[j] – 1] yerine yerleştirilir ve c[j] değeri 1 azaltılır. 242 Teorem 11.7. countingSort(a, k) yöntemi {0, …, k – 1} kümesinden n tamsayıyı içeren a dizisini O(n+k) zamanda sıralayabilir. Sayma-sıralaması algoritmasının istikrarlı olmak gibi güzel bir özelliği vardır; eşit elemanların göreli sırasını korur. İki eleman a[i] ve a[j], aynı değere sahip olursa, ve i < j, o zaman b içinde a[i], a[j]’den önce görünecektir. Bunun, bir sonraki bölümde anlatıldığı üzere yararları vardır. 11.2.2 Taban-Sıralaması Bir tamsayı dizisini sıralayan sayma-sıralaması, dizinin uzunluğu, n, dizideki görünür maksimum değerden, k – 1, çok daha küçük olmadığı zaman çok verimlidir. Şimdi açıklayacağımız taban-sıralaması algoritması, çok daha büyük bir maksimum değer aralığını sağlamak için sayma-sıralamasının birkaç geçişini kullanıyor. Taban-sıralaması w-bit tamsayıları, her geçişte d bit olmak üzere, sıralamak için w/d geçişte sayma sıralamasını kullanır.20 Daha açık olarak, taban-sıralamasında tamsayılar önce en az önemli d bitine göre, daha sonra bir sonraki önemli d bitine göre, ve bu şekilde son seferde en önemli d bitine göre sıralanır. 20 d’nin w’yi böldüğünü varsayıyoruz. Aksi takdirde w, her zaman dw/d olarak artırılabilir. 243 (Bu kodda, (a[i]>>d * p)&((1<<d) – 1) ifadesi ikili gösterimi a[i]’nin (p + 1)d – 1, …,pd biti tarafından verilen tamsayıyı ayıklar.) Bu algoritmanın adımlarının bir örneği Şekil 11.8 'de gösterilmiştir. Bu olağanüstü algoritma doğru sıralar, çünkü sayma-sıralaması istikrarlı bir sıralama algoritmasıdır. a’nın iki elemanı x < y ise ve x’in, y’den farklı olduğu bilinen en önemli biti r endeksine sahipse, o zaman r/d geçişi sırasında x, y’nin önüne yerleştirilecektir ve daha sonraki geçişlerde x ve y’nin göreli sırası değişmeyecektir. Taban-sıralaması w/d geçişten oluşan sayma-sıralaması çalıştırır. Her geçiş O(n+2d) çalışma zamanı gerektirir. Böylece, taban-sıralamasının performansı aşağıdaki teorem tarafından verilmiştir. Teorem 11.8. Herhangi bir tamsayı d > 0 için, radixSort(a, k) yöntemi n adet w-bit tamsayı içeren bir a dizisini O((w/d)(n+2d)) çalışma zamanında sıralayabilir. Bunun yerine, dizinin elemanlarını {0, nc – 1} aralığında düşündüğümüzde ve d = log n aldığımızda Teorem 11.8’in aşağıdaki sürümünü elde ederiz. Sonuç 11.1. radixSort(a, k) yöntemi {0, nc – 1} aralığından n tamsayı içeren bir, a, dizisini O(cn) çalışma zamanında sıralayabilir. 244 11.3. Tartışma ve Alıştırmalar Sıralama bilgisayar biliminin temel algoritmik bir problemidir, ve uzun bir geçmişi vardır. Knuth [48] birleştirerek-sıralama algoritması için von Neumann’a (1945) atıfta bulunuyor. Hızlı-sıralamanın kaynağı Hoare [39] ‘dır. Orijinal yığın-sıralama algoritması Williams [78] tarafından verilmiştir, ancak burada yer alan versiyonun (yığın aşağıdan-yukarıya O(n) zaman içinde oluşturuluyor) kaynağı Floyd [28]’dur. Karşılaştırmaya dayalı sıralama için alt sınır folklor gibi görünür. Aşağıdaki tabloda, karşılaştırmaya dayalı algoritmaların performansları özetleniyor: karşılaştırmalar Birleştirerek-sıralama n log n Hızlı-sıralama 1,38n log n + O(n) Yığın-sıralama en-kötü 2n log n + O(n) beklenen en-kötü yerinde Hayır Evet Evet Bu karşılaştırmaya dayalı algoritmaların her birinin kendi avantajı ve dezavantajı vardır. Birleştirerek-sıralama en az karşılaştırma yapar ve randomizasyona dayanmaz. Ne yazık ki, birleştirme aşamasında bir yardımcı dizi kullanır. Bu diziyi tahsis etmek pahalı olabilir ve bellek sınırlı ise potansiyel bir başarısızlık noktasıdır. Hızlı-sıralama yerinde-işlem yapan bir algoritmadır ve karşılaştırmaların sayısı açısından en yakın ikincidir, ancak randomizedir, bu nedenle bu çalışma zamanı her zaman garanti edilmez. Yığın-sıralama en fazla karşılaştırma yapar, ama yerinde-işlem yapar ve deterministiktir. Birleştirme-sıralamasının net olarak en-önde olduğu bir durum vardır; bağlantılı-listeyi sıralarken bu gerçekleşir. Bu durumda, yardımcı dizi gerekli değildir; sıralı iki bağlantılı-liste işaretçi işlemeleri ile çok kolayca tek bir bağlantılı liste halinde birleştirilebilir (bkz. Alıştırma 11.2). Burada anlatılan sayma-sıralaması ve taban-sıralama algoritmaları Seward tarafından verilmiştir [68, Bölüm 2.4.6]. Ancak, taban-sıralamasının değişik çeşitleri, delikli kart sıralama makinelerini kullanan delikli kartları sıralamak için 1920'lerden beri kullanılmaktadır. Bu makineler kartın belirli bir yerindeki bir deliğin varlığına (ya da 245 yokluğuna) dayalı olarak kart destesini iki yığın halinde sıralayabilirler. Farklı delik yerleri için bu işlemi tekrar etmek taban-sıralamasının bir uygulamasını verir. Son olarak, sayma-sıralama ve taban-sıralamanın negatif olmayan tamsayıların yanı sıra diğer sayı türlerini de sıralamak için kullanılabilir olduğunu not ediyoruz. Basit değişiklikler yaparak sayma sıralama {a, …, b} aralığındaki tamsayıları O(n+ b – a) çalışma zamanında sıralayabilir. Benzer şekilde, taban-sıralama aynı aralıkta tamsayıları O(n (logn(b – a) ) çalışma zamanında sıralayabilir. Son olarak, bu algoritmaların her ikisi de IEEE 754 kayan nokta formatında kayan nokta sayıları sıralamak için kullanılabilir. Bunun nedeni IEEE formatının, iki kayan noktalı sayıyı sanki işaretli-mutlak değer ikili gösteriminde birer tamsayıymış gibi karşılaştırması için tasarlanmış olmasıdır. Alıştırma 11.1. 1,7,4,6,2,8,3,5 içeren bir girdi dizisi için birleştirme-sıralama ve yığınsıralamanın çalışmasını gösterin. Aynı dizide hızlı-sıralamanın örnek bir olası çalışmasını verin. Alıştırma 11.2. Birleştirme-sıralama algoritmasının yardımcı bir dizi kullanmadan Çifte Bağlantılı Liste’yi sıralayan bir versiyonunu uygulayın. (bkz. Alıştırma 3.13.) Alıştırma 11.3. Bazı quickSort(a, i, n, c) uygulamaları her zaman pivot olarak a[i] kullanır. Öyle bir girdi dizisi örneği verin ki, n uzunluğundaki bu dizi için uygulama karşılaştırma gerçekleştirsin. Alıştırma 11.4. Bazı quickSort(a, i, n, c) uygulamaları her zaman pivot olarak a[i + n/2] kullanır. Öyle bir girdi dizisi örneği verin ki, n uzunluğundaki bu dizi için uygulama karşılaştırma gerçekleştirsin. Alıştırma 11.5. Önce a[i], …, a[i + n – 1] değerlerine bakmaksızın pivotu deterministik olarak seçen herhangi bir quickSort(a, i, n, c) uygulamasının karşılaştırma gerçekleştirmesine neden olan n uzunluğunda bir girdi dizisinin her zaman var olduğunu gösterin. 246 Alıştırma 11.6. karşılaştırma gerçekleştiren quickSort(a, i, n, c) uygulamasına bir argüman olarak geçirilen bir Karşılaştırıcı,c, tasarlayın. (İpucu: Karşılaştırıcının karşılaştırılan değerlere gerçekten bakması gerekli değerlidir.) Alıştırma 11.7. Hızlı sıralama tarafından yapılan karşılaştırmaların beklenen sayısını Teorem 11.3’ün kanıtından biraz daha dikkatli olarak analiz edin. Özellikle, karşılaştırmaların beklenen sayısının 2n Hn – n + Hn olduğunu gösterin. Alıştırma 11.8. Yığın-sıralamanın en az 2n log n – O(n) karşılaştırma gerçekleştirmesine neden olan bir girdi dizisi tanımlayın. Cevabınızı doğrulayın. Alıştırma 11.9. Burada anlatılan yığın-sıralama uygulaması elemanlarını ters sırada sıralıyor ve daha sonra diziyi tersine çeviriyor. Bu son adım, girdi Karşılaştırıcı’sının, c, sonucunu olumsuzlayan yeni bir Karşılaştırıcı tanımlayarak önlenebilir. Bunun neden iyi bir optimizasyon olmadığını açıklayın. (İpucu: Bir diziyi tersine çevirmenin ne kadar zaman aldığı ile ilgili olarak, yapılması gereken kaç olumsuzlama gerçekleştirilmesi gerektiğini düşünün.) Alıştırma 11.10. Şekil 11.6’deki karşılaştırma ağacı tarafından doğru sıralanmayan başka bir çift 1, 2, 3 permütasyonu bulun. Alıştırma 11.11. log n! = n log n – O(n) olduğunu kanıtlayın. Alıştırma 11.12. k yapraklı ikili ağacın yüksekliğinin en az log k olduğunu kanıtlayın. Alıştırma 11.13. k yapraklı ikili ağaçtan rasgele bir yaprak almak istersek, o zaman bu yaprağın beklenen yüksekliğinin en az log k olduğunu kanıtlayın. Alıştırma 11.14. Burada verilen radixSort(a, k) uygulaması girdi, a, dizisi, sadece negatif olmayan tamsayıları içerdiği zaman çalışıyor. Hem negatif ve hem de negatif olmayan tamsayıları içerdiğinde de düzgün çalışacak şekilde, bu uygulamayı genişletin. 247 Bölüm 12 Grafikler Bu bölümde, grafiklerin iki gösterimi ve bu gösterimleri kullanan temel algoritmalar inceleniyor. Matematiksel olarak, (yönlü) grafik bir G = (V, E) çiftinden oluşur. Burada, köşelerden oluşan bir küme V, kenar denilen sıralı köşe çiftlerinden oluşan bir küme E tarafından belirtilmektedir. (i, j) ile belirtilen bir kenarın yönü i köşesinden j köşesine doğrudur. i kenarın kaynağı olarak adlandırılır ve j hedef olarak adlandırılır. v0, …, vk köşelerinden oluşan bir dizi ile G grafiğinde bir yol olarak belirtilir, öyle ki her i ∈ {1, …, k} için (vi-1, vi) kenarı E kümesinin elemanıdır. eğer buna ek olarak, (vk, v0) kenarı da E’nin içindeyse v0, …, vk yolu bir döngüdür. Eğer tüm köşeleri benzersiz veya tekil ise, yol (veya döngü) basittir. Herhangi bir vi köşesinden herhangi bir vj köşesine bir yol varsa, vj’nin vi’dan ulaşılabilir olduğunu söyleyebiliriz. Bir grafik örneği Şekil 12.1’de gösterilmektedir. Birçok fenomeni modelleme yetenekleri nedeniyle, grafiklerin büyük sayıda uygulamaları vardır. Çok açık örnekler vardır. Bilgisayar ağları grafik olarak modellenebilir, köşeler bilgisayarlara karşılık gelirken bu bilgisayarlar arasındaki (yönlü) iletişim bağlantılarına kenarlar karşılık gelir. Şehir sokakları grafik olarak modellenebilir, köşeler kavşaklar temsil edilirken, ardışık kavşakları birleştiren sokaklar kenarlar tarafından gösterilir. Daha az belirgin örnekler grafiklerin bir küme içindeki ikili ilişkileri modelleyebildiğini fark ettiğimizde ortaya çıkar. Örneğin bir üniversite ortamında, bir ders programı çatışma grafiği oluşturulabilir, burada köşeler üniversitede sunulan dersleri temsil ederken, hem sınıf i ve hem de sınıf j’yi alan en az bir öğrenci varsa, (i, j) kenarı dahil edilmektedir. Böylece, bir kenarın söylediği, i sınıfı için bir sınav düzenlenecekse aynı saatte j sınıfı için bir sınavın düzenlenmemesi gerektiğidir. 248 Bu bölüm boyunca, G köşe sayısını göstermek için n ve G kenarlarının sayısını göstermek için m kullanacağız. Yani, n = Vve m = E. Ayrıca varsayacağız ki, V = {0, …, n – 1}. V elemanları ile ilişkilendirmek istediğimiz diğer başka veriler n uzunluğunda bir dizide depolanabilir. Grafikler üzerinde yapılan bazı tipik işlemler şunlardır: • addEdge(i, j) • removeEdge(i, j) : E içindeki (i, j) • hasEdge(i, j) : (i, j) ∈ E, • outEdges(i) : (i, j) ∈ E olan • inEdges(i) : (j, i) ∈ E olan : E için bir (i, j) kenarı ekler. kenarını siler. kenar olduğunu kontrol eder. tüm j tamsayılarının bir Liste’sini döndürür. tüm j tamsayılarının bir Liste’sini döndürür. Bu işlemleri verimli uygulamanın çok da zor olmadığını unutmayın. Örneğin, ilk üç işlem Sırasız Küme kullanılarak doğrudan uygulanabilir, bu nedenle Bölüm 5’te ele alınan karma 249 tabloları kullanarak sabit beklenen zamanda uygulanabilir. Son iki işlem, her bir köşe için, bitişik köşelerin bir listesini depolayarak sabit zamanda uygulanabilir. Ancak, farklı grafik uygulamaları için bu işlemlerin farklı performans gereksinimleri vardır, ve, ideal olarak, uygulamanın tüm gereksinimlerini karşılayan en basit uygulamayı kullanmalıyız. Bu nedenle, grafik gösterimlerini iki geniş kategoride tartışacağız. 12.1 Bitişiklik Matrisi: Bir Grafiğin Matris ile Gösterimi Bitişiklik matrisi n köşeli bir G = (V, E) grafiğini elemanları boolean değerler olan n x n matris ile göstermenin bir yoludur. Matris elemanları a[i][j] olarak tanımlanmaktadır. Şekil 12.1’deki grafik için bitişiklik matrisi, Şekil 12.2’de gösterilmiştir. Bu gösterimde, addEdge(i,j), removeEdge(i,j), ve hasEdge(i, j) işlemleri sadece a[i][j] elemanını belirlemeyi veya okumayı içerir: 250 Açıktır ki, bu işlemler işlem başına sabit zaman alır. 251 Bitişiklik matrisinin kötü bir performans sergilediği işlemler outEdges(i) ve inEdges(i) olmuştur. Bunları uygulamak için, a’nın karşılık gelen satır veya sütunundaki bütün n elemanlarını taramamız ve sırasıyla a[i][j] ve a[j][i]’nin true değer olduğu tüm j endekslerini toplamamız gerekmektedir. Açıktır ki, bu işlemler işlem başına O(n) çalışma zamanı alır. Bitişiklik matrisi ile gösterimin diğer bir dezavantajı da büyük olmasıdır. n x n boolean matrisini depolar, bu nedenle en az n2 bit bellek gerektirir. Buradaki uygulama boolean değerlerden oluşan bir matris kullanıyor, bu nedenle, gerçekten n2 bayt büyüklüğünde belleğe ihtiyaç duyuluyor. Her bellek sözcüğünün içine w boolean değerleri paketleyen daha dikkatli bir uygulama, alanı kullanımını O(n2/w) bellek sözcüğüne kadar azaltabilir. Teorem 12.1. Bitişiklik Matrisi veri yapısı Grafik arayüzünü uygular. Bitişiklik Matrisi aşağıdaki işlemleri destekler: • İşlem başına sabit zamanda çalışan addEdge(i, j), removeEdge(i, j), ve hasEdge(i, j); • ve İşlem başına O(n) zamanda çalışan inEdges(i), ve outEdges(i). Bitişiklik Matrisi tarafından kullanılan alan O(n2)’dir. 252 Yüksek bellek gereksinimlerine ve inEdges(i) ve outEdges(i) işlemlerinin kötü performansına rağmen, Bitişiklik Matrisi hala bazı uygulamalarda kullanışlı olabilir. Özellikle, grafik G yoğunsa, yani kenar sayısı n2’ye yakınsa, o zaman n2 bellek kullanımı kabul edilebilir. Bitişiklik Matrisi veri yapısı aynı zamanda yaygın olarak kullanılır, çünkü a matrisi üzerindeki cebirsel işlemler G grafiğinin özelliklerini etkili bir şekilde hesaplamak için kullanılabilir. Bu algoritmalar dersi için bir konudur, ama biz burada şöyle bir özelliği işaret edeceğiz: a’nın girdilerini tamsayı olarak kabul edersek (true için 1 ve false için 0) ve matris çarpımı kullanarak a’nın kendisi ile çarpmasını alırsak o zaman matris a2’yi elde ederiz. Matris çarpımı tanımından hatırlayın ki, bu Bu toplamı G grafiği açısından yorumlarsak, G hem (i, k) ve hem (k, j) kenarlarını içerirken bu formül köşe sayısını, k, sayar. Yani, ara köşeler, k, üzerinden uzunluğu tam olarak iki olan i'den j’ye yolları sayar. Bu gözlem, , sadece O(log n) matris çarpımı kullanarak G’nin bütün köşe çiftleri arasındaki en kısa yolları hesaplayan bir algoritmanın temelini oluşturur. 12.2 Bitişiklik Listeleri: Liste Derlemi olarak Grafik Bitişiklik listeleri ile grafik gösterimi daha köşe-merkezli bir yaklaşım gerektirir. Bitişiklik listelerinin birçok olası uygulamaları vardır. Bu bölümde, basit bir tanesini sunuyoruz. Bölümün sonunda, farklı olasılıkları ortaya koyacağız. Bitişiklik listeleri gösteriminde G = (V, E) grafiği bir liste dizisi, adj, olarak temsil edilir. adj[i] listesi i köşesine bitişik tüm köşelerin bir listesini içerir. Yani, (i, j) ∈ E olan her j endeksini içerir. (Bir örnek Şekil 12.3’de gösterilmiştir.) Bu özel uygulamada, adj içindeki her liste bir Dizi Yığıt olarak temsil edilmiştir, çünkü konuma göre erişim için sabit zaman gereksinimini 253 sağlamak istiyoruz. Diğer seçenekler de mümkündür. Özellikle, adj bir Çifte Bağlantılı Liste olarak da gerçekleştirilebilirdi. addEdge(i, j) işlemi sadece j değerini adj[i] listesine ekler: Bu sabit zaman alır. 254 removeEdge(i, j) işlemi j değerini adj[i] listesinde bulana kadar arar, ve bulduğunda siler: Bu, O(der(i)) zaman alır, burada der(i), (i derecesi) kaynağı i olan E kenarlarının sayısını sayar. hasEdge(i, j) işlemi de benzerdir; j değerini adj[i] listesinde bulana kadar arar (ve true döndürür), veya listenin sonuna ulaşır (ve false döndürür): Bu da O(der(i)) zaman alır. outEdges(i) işlemi çok basittir; adj[i] listesini döndürür: Bu açık bir şekilde, sabit bir zaman alır. inEdges(i) işlemi daha çok çalışma gerektirir. Her j köşesinin üzerinden tarayarak (i, j) kenarının bulunup bulunmadığı kontrol edilir, ve eğer bulunuyorsa, çıktı listesine j eklenir: 255 Bu işlem çok yavaştır. Her köşenin bitişiklik listesini tarar, bu nedenle O(n + m) zaman alır. Aşağıdaki teorem yukarıdaki veri yapısının performansını özetler: Teorem 12.2. Bitişiklik Listeleri veri yapısı Grafik arayüzünü uygular. Bitişiklik Listeleri aşağıdaki işlemleri destekler: • İşlem başına sabit zamanda çalışan addEdge(i, j); • İşlem başına O(der(i)) zamanda çalışan removeEdge(i, j) ve hasEdge(i, j); • İşlem başına sabit zamanda çalışan outEdges(i); ve • İşlem başına O(n + m) zamanda çalışan inEdges(i). Bitişiklik Listeleri tarafından kullanılan alan O(n + m) olur. Daha önce değinildiği gibi, bir grafiği bitişiklik listesi olarak uygularken yapılacak pek çok farklı seçenek vardır. Aklımıza gelen bazı sorular şunlardır: • Her adj elemanını saklamak için ne tür bir derlem kullanılmalıdır? Bir dizi-tabanlı liste, bir bağlantılı liste, hatta bir karma tablosu kullanılabilir. • Her i köşesi için (j, i) ∈ E olan j köşe listesini saklayan ikinci bir bitişiklik listesi, inadj, olmalı mıdır? Böylece, inEdges(i) çalışma zamanını büyük ölçüde azaltılabilir, ancak kenarları eklerken veya silerken biraz daha fazla çalışma gerektirir. • adj[i] içindeki (i, j) kenar girdisi kendisine inadj[j] içinde karşılık gelen girdiye bir referans vererek bağlanmalı mıdır? 256 • Kenarlar kendi ilişkili verileri ile birinci sınıf nesneler olmalı mıdır? Bu şekilde, adj köşeler (tamsayılar) listesinden ziyade kenarların listelerini içerecektir. Bu soruların çoğu uygulamanın karmaşıklığı (ve gerektirdiği alan) ile performans özelliklerinden birini elde ederken diğerinden vazgeçmeyi beraberinde getirir. 12.3. Grafik Ziyaretleri Bu bölümde bir grafiği keşfetmek için grafiğin, i, köşesinden başlayarak, i’den ulaşılabilir bütün köşelerini bulan iki algoritma sunuyoruz. Bu algoritmaların ikisi de bitişiklik listesi gösterimini kullanarak temsil edilen grafikler için uygundur. Bu nedenle, bu algoritmalar analiz edilirken temel gösterimin Bitişiklik Listeleri olduğunu kabul edeceğiz. 12.3.1. Enine-Arama Enine-arama algoritması, i, köşesinden başlar ve ilk olarak, i’nin komşularını ziyaret eder, sonra i’nin komşularının komşularını ziyaret eder, sonra i’nin komşularının komşularının komşularını ziyaret eder, ve benzeri. Bu algoritma ikili ağaçlar için enine-arama algoritmasının (Bölüm 6.1.2) bir genellemesi ve çok benzeridir; başlangıçta sadece i’yi içeren bir kuyruk, q, kullanır. Daha sonra tekrar tekrar q’nin bir elemanını alır ve bu elemanın komşularının daha önce q içinde olmaması şartıyla, komşularını q’ya ekler. Grafikler ve ağaçlar için enine-arama algoritması arasındaki tek büyük fark, grafikler için tasarlanan algoritmanın birden fazla aynı kez q köşe noktasını eklememesidir. Bunu sağlarken, hangi köşelerin zaten keşfedilmiş olduğunu tutan yardımcı bir boolean dizi, seen, kullanır. 257 Şekil 12.1’deki grafik üzerinde bfs(g, 0) çalıştırılmasının bir örneği Şekil 12.4’de gösterilmiştir. Bitişiklik listelerinin sıralanmasına bağlı olarak farklı çalıştırmalar mümkündür. Şekil 12.4, Şekil 12.3’teki bitişiklik listelerini kullanmaktadır. bfs(g, i) rutininin çalışma zamanını analiz etmek oldukça basittir. seen dizisinin kullanılmasıyla, q köşelerinin hiçbiri birden fazla eklenmez. Her q köşesini eklemek (ve daha sonra silmek) köşe başına sabit zaman alır, toplamda bu O(n) zaman eder. Her bir köşe en fazla bir kere iç döngü tarafından işlendiği için, her bitişiklik listesi, en fazla bir kez işlenir, 258 yani G’nin her bir kenarı en fazla bir kere işlenir. İç döngü içinde yapılan bu işlem, yineleme başına sabit zaman alır ve toplamda O(m) zaman eder. Bu nedenle, algoritmanın tamamı O(n+m) zamanda çalışır. Aşağıdaki teorem bfs(g, r) algoritmasının performansını özetlemektedir. Teorem 12.3. Bitişiklik Listeleri veri yapısı kullanılarak uygulanan bir girdi Grafik, g, verildiğinde bfs(g, r) algoritması O(n+m) zamanda çalışır. Bir enine-aramanın çok özel bazı özellikleri vardır. bfs(g, r) çağrısı en sonunda her j köşesini r’den j’ye yönlü bir yol olacak şekilde kuyruğa ekler (ve en sonunda kuyruktan çıkarır). Ayrıca, r’ye 0 mesafesindeki köşeler (r’nin kendisi) 1 mesafesindeki köşelerden önce q içine girecektir. 1 mesafesindeki köşeler, 2 mesafesindeki köşelerden önce q içine girecektir, ve benzeri. Bu nedenle, bfs(g, r) yöntemi r’ye olan mesafesi artan sırada olacak şekilde köşeleri ziyaret eder, ve r tarafından ulaşılamayan köşeler asla hiç ziyaret edilmez. Enine-arama algoritmasının özellikle yararlı bir uygulaması, bu nedenle, en kısa yolları hesaplamaktır. Her köşe için, r’den en kısa yolu hesaplamak için, n uzunluğunda bir yardımcı p dizisi kullanan değişik bir bfs(g, r) kullanıyoruz. Yeni köşe j, q’ya eklendiğinde, p[j] = i olarak belirlenir. Bu şekilde, p[j], r’den j’ye en kısa yol üzerindeki ikinci son düğüm olur. Bunun tekrarlanmasıyla, p[p[j], p[p[p[j]]], ve benzerini alarak, r’den j’ye en kısa bir yolu (ters yönde) yeniden oluşturabiliriz. 12.3.2. İlkin-Derinliğine Arama İlkin-derinliğine-arama algoritması ikili ağaçları ziyaret etmek için tasarlanan standart algoritmaya benzer; geçerli düğüme dönmeden önce ilk bir altağacı tamamen araştırır, ve daha sonra diğer altağacı araştırır. İlkin-derinliğine-aramayı düşünmek için başka bir yol enine-aramanın bir nokta dışında benzeridir, kuyruk yerine yığıt kullanır. İlkin-derinliğine-arama algoritmasının çalışması sırasında, her i köşesine, bir renk, c[i] atanır: daha önce köşeyi görmemişsek white (beyaz), şu anda bu köşeyi ziyaret ediyorsak gray (gri), 259 ve bu köşeyi ziyaret ettiysek black (siyah). İlkin-derinliğine-aramayı düşünmenin en kolay yolu onu bir özyinelemeli algoritma olarak düşünmektir. r köşesini ziyaret ederek başlarız. i köşesini ziyaret ederken, ilk olarak i gri olarak işaretlenir. Sonra, i'nin bitişiklik listesini tararız ve özyinelemeli olarak bu listede bulabildiğimiz herhangi bir beyaz köşeyi ziyaret ederiz. Son olarak, i işlenmesi tamamlandığında, i siyah renkle işaretlenir ve dönüş yapılır. Bu algoritmanın çalıştırmasının bir örneği Şekil 12.5’de gösterilmiştir. 260 İlkin-derinliğine-arama iyi bir özyinelemeli algoritma olarak düşünülebilir olsa da, özyineleme bunu uygulamak için en iyi yol değildir. Nitekim, yukarıda verilen kod birçok büyük grafik için yığıt taşmasına yol açarak başarısız olur. Alternatif bir uygulama özyineleme yığıtını, açık bir yığıt ile, s, ile değiştirmektir. Aşağıdaki uygulama tam olarak bunu yapar: Yukarıdaki kodda, bir sonraki köşe, i, işlendiğinde, i gri renge boyanır ve sonra, yığıt üzerinde, bitişik köşeler ile yer değiştirir. Sonraki yineleme sırasında, bu köşelerden biri ziyaret edilecektir. Beklendiği gibi, dfs(g, r) ve dfs2(g, r) çalışma zamanları bfs(g, r) ile aynıdır: Teorem 12.4. Bitişiklik Listeleri veri yapısı kullanılarak uygulanan bir girdi Grafik, g, verildiğinde dfs(g, r) ve dfs2(g, r) algoritmalarının her biri O(n+m) zamanda çalışır. Enine-arama algoritmasında olduğu gibi, ilkin-derinliğine-aramanın her çalıştırması ile ilişkili altta yatan bir ağaç vardır. i ≠ r olan bir düğüm beyazdan griye doğru gittiğinde bunun nedeni dfs(g, i, c) bir i’ düğümünü işlerken özyinelemeli olarak çağrılmıştır. (dfs2(g, r) algoritmasında, i yığıtta i’ yerini alan düğümlerden biridir.) Eğer i menşei olarak i’ düşünülürse, o zaman r köklü bir ağaç elde edilir. Şekil 12.5’te bu ağaç köşe 0’dan köşe 11 için bir yoldur. İlkin-derinliğine-aramanın önemli bir özelliği şudur: Varsayalım ki, düğüm i gri renkli olduğunda, i’den diğer bazı j düğümü için sadece beyaz köşeleri kullanan bir yol vardır. O 261 zaman j ilk olarak gri renkli olacak, sonra i siyah olmadan önce siyah renkli olacaktır. (Bu, i’den j’ye herhangi bir, P, yolunu dikkate alarak çelişki yöntemi ile kanıtlanabilir.) Bu özelliğin bir uygulaması döngülerin saptanmasıdır. Şekil 12.6’ya bakın. r’dan ulaşılabilir bir, C, döngüsünü düşünün. i, C'nin ilk gri renkli düğümü olsun, ve C döngüsü üzerinde i öncesindeki düğüm j olsun. Daha sonra, yukarıda belirtilen özellik ile, j gri renkli olacak ve i hala gri iken (j, i) kenarı algoritma tarafından kabul edilecektir. Bu nedenle, algoritma ilkinderinliğine-arama ağacında i’den j’ye bir yol, P, olduğu ve (j, i) kenarının bulunduğu sonucuna varabilir. Bu nedenle, P, aynı zamanda bir döngüdür. 12.4. Tartışma ve Alıştırmalar İlkin-derinliğine-arama ve enine-arama algoritmalarının çalışma zamanları Teorem 12.3 ve 12.4 tarafından biraz abartılmıştır. nr , r’dan i için yolu var olan G’nin i köşelerinin sayısını belirtsin. mr bu köşeleri kaynak olarak alan kenarların sayısını belirtsin. O zaman, aşağıdaki teorem enine-arama ve ilkin-derinliğine-arama algoritmalarının çalışma sürelerini veren daha kesin bir ifadedir. (Çalışma zamanlarının bu daha rafine ifadesi alıştırmalarda özetlenen bu algoritmaların bazı uygulamalarında faydalıdır.) Teorem 12.5. Bitişiklik Listeleri veri yapısı kullanılarak uygulanan bir girdi Grafik, g, verildiğinde bfs(g, r), dfs(g, r) ve dfs2(g, r) algoritmalarının her biri O(nr+mr) zamanda çalışır. 262 Enine-arama Moore [52] ve Lee [49] tarafından sırasıyla labirent keşif ve devre yönlendirme gibi bağlamlarda bağımsız olarak keşfedilmiştir. Grafiklerin Bitişiklik Listesi gösterimleri Hopcroft ve Tarjan [40] tarafından (o zaman daha yaygın olan) Bitişiklik Matrisi gösterimi için bir alternatif olarak sunuldu. Bu temsil, ilkinderinliğine-arama ile birlikte, kenarlarının herhangi bir çifti birbirine çapraz olmayan bir grafiğin [41] O(n) zamanda bir düzlemde çizilip çizilemeyeceğini belirleyen, ünlü HopcroftTarjan düzlemsellik test algoritmasında önemli bir rol oynadı. Aşağıdaki alıştırmalarda, yönsüz grafik, her i ve j için (i, j) kenarının ancak ve ancak (j, i) kenarı mevcutsa mevcut olan bir grafiktir. Alıştırma 12.1. Şekil 12.7’deki grafiğin Bitişikliklik Listesi gösterimini ve Bitişiklik Matrisi gösterimini çizin. Alıştırma 12.2. G grafiğinin geliş matrisi gösterimi n x m matrisi, A, olarak aşağıdaki gibi tanımlanır. 263 1. Şekil 12.7’deki grafiğin geliş matrisi gösterimini çizin. 2. Bir grafiğin geliş matrisi gösterimini tasarlayın, analiz edin ve uygulayın. addEdge(i, j), removeEdge(i, j), hasEdge(i, j), inEdges(i), ve outEdges(i) çalışma zamanları ve alan gereksinimini analiz etmeniz gerekiyor. Alıştırma 12.3. Şekil 12.7’deki, G, grafiği üzerinde bfs(G, 0) and dfs(G, 0) çalıştırmasını gösterin. Alıştırma 12.4. G yönsüz bir grafik olsun. G grafiğinin her i, j köşe çifti için i’den j’ye bir yol varsa (G yönsüz grafik olduğu için j’den i’ye de bir yol vardır) bağlanmış olduğu söylenir. O(n+m) zamanda G’nin bağlanmış olduğunun nasıl test edileceğini gösterin. Alıştırma 12.5. G yönsüz bir grafik olsun. G’nin bağlanmış-bileşenli etiketlemesi G köşelerini büyükçe kümeler halinde bölümler. Bunların her biri, bağlanmış bir alt grafiği oluşturur. O(n+m) zamanda G’nin bağlanmış-bileşenli etiketlemesinin nasıl hesaplanacağını gösterin. Alıştırma 12.6. G yönsüz bir grafik olsun. G’den yayılan orman ağaçları, bileşen başına kenarları G kenarları olan ve köşeleri G’nin bütün köşelerini içeren ağaçların bir derlemidir. O(n+m) zamanda G’nin yayılan ormanının nasıl hesaplanacağını gösterin. Alıştırma 12.7. G grafiğinin her i, j köşe çifti için i’den j’ye bir yol varsa kuvvetli-bağlanmış olduğu söylenir. O(n+m) zamanda G’nin kuvvetli-bağlanmış olduğunun nasıl test edileceğini gösterin. Alıştırma 12.8. G = (V, E) grafiği ve bazı özel köşe r ∈ V için r’dan i için her i ∈ V köşesi için en kısa yol uzunluğunun nasıl hesaplanacağını gösterin. Alıştırma 12.9. dfs(g, r) kodunun bir grafiğin düğümlerini dfs2(g, r)’den farklı olan bir sıra içinde ziyaret etmesine (basit) bir örnek verin. dfs(g, r) ile tam olarak aynı sırada düğümleri ziyaret eden dfs2(g, r)’in bir versiyonunu yazın. (İpucu: r’in kaynağı olduğu kenar sayısı 1’den fazla iken her iki algoritmanın herhangi bir grafik üzerinde çalışmasını izleyin.) 264 Alıştırma 12.10. G grafiğinin evrensel çıkış düğümü n – 1 kenarın hedefi olan, ancak hiçbir kenarı olmayan bir köşedir.21 Bitişiklik Matrisi ile gösterilen bir G grafiğinin evrensel çıkış düğümünün var olduğunu test eden bir algoritma tasarlayın ve uygulayın. 21 Evrensel çıkış düğümü, v, özel bir düğümdür: Herkes v’yi odada tanıyor iken v odadaki kimseyi tanımıyor. 265 Bölüm 13 Tamsayılar için Veri Yapıları Bu bölüm Sıralı Küme veri yapısı ve uygulaması ile ilgilidir. Sıralı Küme’de saklanan elemanlar w-bit tamsayılar olarak kabul edilmiştir. Yani, x ∈ {0, 2w – 1} için ekle(x), sil(x), ve bul(x) işlemlerini uygulamak istiyoruz. Verinin – ya da en azından veri sıralamak için kullandığımız anahtarın – tamsayı olduğu pek çok uygulama olduğunu düşünmek zor olmasa gerekir. Önce gelen fikrin bir sonraki fikri oluşturduğu üç veri yapısını tartışacağız. İlk yapı, İkili Sıralı Ağaç her zaman Sıralı Küme işlemlerinin üçünü birden O(w) zamanda gerçekleştirir. Bu beklentimiz dahilindedir, çünkü {0, …, 2w – 1} içinden herhangi bir alt küme n ≤ 2 w boyutuna sahiptir, ve böylece log n ≤ w. Bu kitapta değinilen Sıralı Küme uygulamalarının hepsi O(log n) zamanda çalışır. Bu yüzden hepsi en azından bir İkili Sıralı Ağaç gibi hızlıdır. İkinci yapı X-Hızlı Sıralı Ağaç, karmayı kullanarak İkili Sıralı Ağaç içinde yapılan bir aramayı hızlandırır. Bu hızın arttırılması ile, bul(x) işlemi O(log w) zamanında çalışır. Ancak, X-Hızlı Sıralı Ağaç içindeki ekle(x) ve sil(x) işlemleri hala O(w) zaman alabilir ve X-Hızlı Sıralı Ağaç tarafından kullanılan alan O(n . w) olur. Üçüncü yapı, Y-Hızlı Sıralı Ağaç her w elemanı içinden X-Hızlı Sıralı Ağaç’ı kullanarak sadece bir örnek depolar ve kalan elemanları standart bir Sıralı Küme yapısında saklar. Böylece ekle(x) ve sil(x) çalışma zamanı O(log w) ve alan gereksinimi O(n) olur. Bu bölümde örnek olarak kullanılan uygulamalar, tamsayı ile bağlantı edilebilen her türlü veriyi saklayabilir. Kod örneklerinde tamsayı olarak x değeri kullanılmıştır. x ile ilişkili tamsayı değeri, her zaman değişken ix ile gösteriliyor, ve in.intValue(x) yöntemi x ile ilişkili tamsayı değerini döndürüyor. 266 13.1. İkili Sıralı Ağaç: Sayısal Arama Ağacı İkili Sıralı Ağaç, ikili ağaç içindeki w-bit tamsayıları şifreler. Ağaçtaki tüm yaprakları w derinliğindedir ve her tamsayı kökten-yaprağa bir yol olarak kodlanmıştır. i’nci en anlamlı bit, 0 ise, x tamsayısı için yol i seviyesinde sola doğru gider. 1 ise sağa gider. Şekil 13.1 w = 4 durumu için tamsayılar 3 (0011), 9 (1001), 12 (1100), 13 (1101) depolandığı örnek bir ağaç gösteriyor. Herhangi bir x değeri için arama yolu x bitlerine bağlı olduğu içindir ki, bir düğümün çocuklarını u, u.child[0] (sol) ve u.child[1] (sağ) olarak isimlendirmek yararlı olacaktır. child olarak adlandırılan bu işaretçilerin birden çok fonksiyonu vardır. İkili Sıralı Ağaç’ın yapraklarında çocuk yoktur. İşaretçilerin fonksiyonu yaprakları bir araya getirmektir. Bunun için bir Çifte-Bağlantılı Liste (ÇBListe) yaprakların sırasını kaydetmeye yarayabilir. İkili Sıralı Ağaç içindeki bir yaprak için, u.child[0] (prev) listede u öncesine referans verir. u.child[1] (next) listede u sonrasına referans verir. Özel bir düğüm, dummy, hem ilk düğümün öncesine, hem son düğümün sonrasına referans verir (Bkz. Bölüm 3.2). Her, u, düğümü ayrıca u.jump adında bir ek işaretçi içerir. u düğümü için solundaki çocuk mevcut değilse, u.jump, u altağacındaki en küçük yaprağa işaret eder. Sağındaki çocuk mevcut değilse, u.jump, u altağacındaki en büyük yaprağa işaret eder. jump işaretçilerini 267 gösteren bir İkili Sıralı Ağaç örneği ve yaprakları Çifte-Bağlantılı Liste halinde Şekil 13.2’de gösterilmiştir. İkili Sıralı Ağaç’ın bul(x) yöntemi oldukça kolaydır. Arama yolunun başlangıcında u düğümleri arasından x yaprağı aranır. Eğer öyleyse, x bulunmuş olur. u düğümüne ulaşmaya devam edilemezse, (x çocuğu yoksa, yani yaprağa ulaşıldıysa), u.jump yolu aranır. Bu arama bizi x’den büyük en küçük yaprağa, yada x’den küçük en büyüğe götürür. Bu iki durumdan hangisinin önce oluşmasını, u düğümünün sol mu sağ mı çocuğunun, sırasıyla, eksik yada var olmasına bağlayabiliriz. Birinci durumda (u, sol çocuğu eksik), istediğimiz düğüm bulunmuş 268 olur. İkinci durumda (u, sağ çocuğu eksik), istediğimiz düğüme ulaşmak için bağlantılı liste yardımcı olacaktır. Bu durumların her biri, Şekil 13.3’te gösterilmiştir. bul(x) yönteminin çalışma zamanı kökten yaprağa aldığı yol cinsinden O (w) olur. İkili Sıralı Ağaç’ın ekle(x) işlemi kolay, ancak oldukça zahmetlidir: 1. Arama yolu üzerinde u düğümü için ulaşılabilen son düğüme kadar arama yapılır. 2. u düğümünden başlayarak arama yolunun kalanı için, x içeren bir yaprağa kadar arama yolu kaydedilir. 269 3. x içeren bir u’ düğümü oluşturularak bağlantılı yaprak listesine eklenir (Birinci adımda en son ziyaret edilen, u, düğümüne ait olan jump işaretçisinin gösterdiği bağlantılı listenin içinden u’ öncesine, pred, erişim sağlanmalıdır.) 4. x arama yolu üzerindeki jump işaretçilerinin x referansını gösteren tüm değişiklikler yapılır. 270 Şekil 13.4’te örnek bir ekleme çalıştırılması gösteriliyor. Bu yöntemin çalıştırılması sırasında x için önce ileri, sonra geri yönde birer arama yolu bulunuyor. Her iki takip de sabit zamanda çalışır, bu nedenle ekle(x) yönteminin çalışma zamanı O(w) olur. sil(x) bir anlamda ekle(x) için yapılanları yok eder. Şu adımlar u düğümünü silmeye yarar: 1. u düğümü arama yolu üzerinden x içeren yaprağa ulaşana kadar aranmalıdır. 2. Çifte-bağlantılı listeden u silinmelidir. 3. u silindikten sonra x için arama yolu üzerinde olmayan bir çocuğa sahip v düğümü ulaşılana kadar arama yolu ardı sıra silinir. 4. u’ya işaret veren her jump işaretçisini güncellemek için başlangıcı v sonu kök olan yeni bir yol oluşturulur. Şekil 13.5’te örnek bir silme çalıştırılması gösteriliyor. Teorem 13.1. İkili Sıralı Ağaç w-bit tamsayılar için Sıralı Küme arayüzünü uygular. İkili Sıralı Ağaç işlem başına O(w) zamanda çalışan ekle(x), sil(x), ve bul(x) işlemlerini destekler. n değer içeren İkili Sıralı Ağaç’ın kullanması gerekli alan O(n . w) kadardır. 271 13.2 X-Hızlı Sıralı Ağaç: Çifte-Logaritmik Zamanda Arama X-Hızlı Sıralı Ağaç’ın yapısının performansı çok etkileyici değildir. Veri yapısında depolanan eleman sayısı, n, en çok 2W olarak kabul edilirse, log n ≤ w olur. Diğer bir deyişle, bu kitabın diğer bölümlerinde açıklanan karşılaştırmaya-dayalı Sıralı Küme yapılarının herhangi biri en az İkili Sıralı Ağaç kadar verimlidir, ve sadece tamsayı depolama ile sınırlı değildir. 272 Bu bölümde X-Hızlı Sıralı Ağaç yapısı tanıtılıyor. Bu yapıdaki sıralı ağacın her seviyesi birer w+1 karma tabloya sahiptir ve yapı İkili Sıralı Ağaç’a çok benzerdir. Bu karma tablolar ile bul(x) işleminin O(log w) zamanda çalışması mümkündür. Hatırlayın ki, İkili Sıralı Ağaç’ta bulunan x için bul(x) işlemi sırasında arama yolu u düğümü için u.right yada u.left tarafına yöneldiğinde, ancak u.right ve u.left çocukları yoksa arama neredeyse tamamlanıyordu. Bu noktada, arama u.jump yardımıyla bir yaprağa ağaç üzerinden atlama yapar ve ya v yada yaprakları depolayan bağlantılı-liste içinden v’den sonra geleni döndürür. X-Hızlı Sıralı Ağaç u düğümünü bulmak için ağacın her düzeyinde ikili arama prosedürünü gerçekleştirerek arama işlemini hızlı hale getirir. Burada, aranan u düğümünün belirli seviyelerin üstünde mi yoksa altında mı veya neresinde sorusu için ikili arama gerçekleştiriliyor. x ikili gösteriminde en önemli i’nci bit kökten i’nci seviyeye çizilen arama yolunu belirlemede gereklidir. Şekil 13.6’a göre 14 sayı değerine (ikili gösterimi 1110) ait arama yolu üzerindeki son, u, düğümü 2.seviyede düğümdür, çünkü 3.seviyede ile etiketlenmiş ile etiketli herhangi bir düğüm yoktur. Böylece, i seviyesindeki her düğüm i-bitlik bir tamsayı tarafından etiketlenir. Aradığımız u düğümünün i seviyesinde veya altında mı olduğu ancak ve ancak i düzeyindeki etiketin x’nin en önemli i-bitine karşılık gelmesi ile mümkün olur. 273 Her i ∈ {0, …, w} seviyesi için X-Hızlı Arama Ağacı’nda karma tabloyu uygulayan t[i] düğümlerinin hepsi Sırasız Küme’nin içinde saklanır (Bölüm 5). Bu Sırasız Küme sayesinde i düzeyinde x’in en önemli i-biti ile etiketlenmiş bir düğüm olduğunu kontrol etmek beklenen bir sabit zamanda gerçekleşir. Bunu düğüm t[i].bul(x >> (w – i)) sonucuna göre de belirleyebiliriz. t[0] … t[w] karma tabloları üzerinde yapılan ikili arama gerçekleştirimiyle u’nun yeri belirlenir. Başlangıçta, u’nun 0 < i < w+1 seviyesinde olduğu biliniyor. Bu nedenle çalıştırmaya l = 0 ve h = w + l ile başlanır ve i = b(l + h)=2c denklemini sağlayan t[i] karma tablosunda x’in en önemli i-biti ile etiketlenmiş düğümün içerildiğini kontrol ederiz. Doğruysa, l = i belirlemesi yapılır (u, i seviyesinde veya altındadır.); aksi takdirde, h = i belirlemesi yapılır (u, i seviyesinin üstündedir). h – l ≤ 1 koşulu bu işlemi sonlandırır; çünkü l seviyesinde u’nun yeri belirlenmiştir. Daha sonra u.jump ile yapraklara ulaşılır, bağlantılı listede yapılan bir arama ile bul (x) işlemi tamamlanır. Yukarıdaki yöntemde while döngüsü içinde işlem gören her h – 1 yinelemesi arama mesafesini yaklaşık olarak 2 faktör oranında azaltır. Bu nedenle döngünün O(log w) tekrarından sonra u beklenen sabit bir zamanda bulunur. Kalan çalışmalar sadece sabit zaman alır, böylece X-Hızlı Sıralı Ağaç’ın bul(x) yöntemi O(log w) beklenen zamanda çalışır. 274 X-Hızlı Sıralı Ağaç’ın ekle(x) t[0] …. t[w] ve sil(x) yöntemleri İkili Sıralı Ağaç ile hemen hemen aynıdır. karma tabloları için sadece yönetimsel değişiklikler yapılması yeterlidir. ekle(x) işlemi sırasında yeni bir düğüm i seviyesinde oluşturulduğu zaman, veya sil(x) yönteminin çalıştırılması sırasında bir düğüm i seviyesinde silindiğinde bu düğüm t[i] tablosundan silinir. Bir karma tabloya değer eklenmesi veya silinmesi sabit beklenen zaman aldığı için, ekle(x) ve sil(x) çalışma zamanlarındaki artış sabit bir faktör ile sınırlıdır. ekle(x) ve sil(x) kod dökümü burada dahil edilmemiştir, çünkü kod listesi İkili Sıralı Ağaç’ın yöntemleri ile hemen hemen aynı satırlardan oluşmaktadır. Aşağıdaki teorem X-Hızlı Sıralı Ağaç performansını özetler: Teorem 13.2. X-Hızlı Sıralı Ağaç w-bit tamsayılar için Sıralı Küme arayüzünü uygular. X-Hızlı Sıralı Ağaç n aşağıdaki işlemleri destekler: • İşlem başına O(w) beklenen zamanda çalışan ekle(x), sil(x) ve • İşlem başına O(log w) beklenen zamanda çalışan bul(x). değer içeren X-Hızlı Sıralı Ağaç’ın kullanması gerekli alan O(n . w) kadardır. 13.3 Y-Hızlı Sıralı Ağaç: Çifte-Logaritmik Zamanlı Sıralı Küme X-Hızlı Sıralı Ağaç, sorgu zamanı açısından İkili Sıralı Ağaç ile kıyaslandığında oldukça büyük –hatta üstel – bir gelişme sağlar, ancak ekle(x) ve sil(x) işlemleri hala istenildiği kadar hızlı değildir. Ayrıca, alan kullanımı, O(n . w), bu kitapta anlatılan ve kullanım alanı O(n) olan tüm diğer Sıralı Küme uygulamalarından daha yüksektir. Bu iki sorun ilişkilidir; ekle(x) işlemi n kere çalıştırılarak boyutu n . w olan bir yapı oluşturulursa, o zaman ekle(x) işlemi, en azından işlem başına w büyüklüğünde bir zaman (ve alan) gerektirir. Bir sonraki tartışılan Y-Hızlı Sıralı Ağaç, X-Hızlı Sıralı Ağaç’ın aynı anda hem alanını azaltır ve hem de hızını artırır. Bir Y-Hızlı Sıralı Ağaç, X-Hızlı Sıralı Ağaç’ı, xft, kullanır, ancak xft içinde sadece O(n/w) değer depolar. Bu şekilde, xft tarafından kullanılan toplam alanı sadece 275 O(n) olur. Ayrıca, Y-Hızlı Sıralı Ağaç içindeki her w ekle(x) yada sil(x) işleminden sadece biri xft içinde bir ekle(x) veya sil(x) işlemiyle sonuçlanır. Bunu yaparak xft’nin ekle(x) ve sil(x) işlemlerine yapılan çağrıların ortalama maliyeti sadece sabittir. Açık soru şudur: xft sadece n/w eleman depoluyorsa, geride kalan n(1 – 1/w) eleman nereye gider? Bu elemanlar ikincil yapıların içine taşınmıştır, bu durumda genişletilmiş bir treaps versiyonu (Bölüm 7.2) içine gitmiştir. Bu ikincil yapılardan kabaca n/w tane vardır, ortalama olarak, her biri O(w) eleman depolar. Treaps, logaritmik zamanda Sıralı Küme işlemlerini desteklemektedir, böylece bu Treap’ler üzerindeki işlemler gerektiği gibi, O(log w) zamanda çalışacaktır. Daha somut olarak, bir Y-Hızlı Sıralı Ağaç bu verilerdeki her elemanın bağımsız olarak 1/w olasılıkla göründüğü bir rasgele örneğini içeren bir X-Hızlı Sıralı Ağaç, xft, içerir. Kolaylık sağlamak için, değer 2w - 1, her zaman xft içindedir. Düşünün ki, xft içinde saklanan elemanlar x0 < x1 < ... < xk-1 ile ifade edilsin. O zaman Şekil 13.7’de gösterildiği gibi, her eleman, xi, ile ilişkili, xi–1 + 1, ..., xi aralığındaki tüm değerleri depolayan bir treap, ti, vardır. 276 Bir Y-Hızlı Sıralı Ağaç içinde bul(x) kullanımı oldukça kolaydır. xft içinde x aranır ve treap, ti, ile ilişkili bazı xi değeri bulunur. Sonra sorguyu cevaplandırmak için treap(x) yöntemi ti için çalıştırılır. (xft üzerinde) ilk bul(x) işlemi O(log w) zamanda çalışır. (treap üzerinde) ikinci bul(x) işlemi, r treap boyutu olarak verildiğinde, O(log r) çalışma zamanı alır. Bu bölümde sonra, treap’in beklenen boyutunun O(w) olduğunu göstereceğiz, böylece bu işlem O(log w) çalışma zamanı alacaktır.22 Y-Hızlı Sıralı Ağaç içine bir eleman eklemek de çoğu zaman oldukça kolaydır. Önce x’in içine gireceği treap, t, bulunmalıdır. ekle(x) yöntemi xft.bul(x) işlemini çağırır. Daha sonra x, t içine eklenmelidir. Bunu gerçekleştirmek için t.ekle(x) çağrılır. Bu noktada, 1/w olasılığıyla 22 Bu Jensen Eşitsizliği’nin bir uygulamasıdır: E[r] = w ise E[log r] ≤ log w olur. 277 tura gelen ve 1 – 1/w olasığıyla yazı gelen hileli para fırlatılır. Bu para tura geldiyse, o zaman xft içine x eklenecektir. İşlerin biraz daha karışık hale geldiği yer burasıdır. xft içine x eklendiğinde, treap t iki treaps haline, t1 ve t’ olmak üzere, ayrılmış olmalıdır. Tüm treap t1 değerleri, x’den daha az veya eşittir. Orijinal treap, t, içinden t1 elemanlarının dışında kalan elemanlar ise t’ içinde depolanır. Bu yapıldıktan sonra, xft içine (x, t1) çifti eklenir. Şekil 13.8 bir örneği göstermektedir. t içine x eklenmesi O(log w) zamanda çalışır. Alıştırma 7.12, t1 ve t’ halinde t ayrılmasının da O(log w) zamanda yapılabileceğini gösteriyor. xft içine (x, t1) çiftinin eklenmesi O(w) zaman alır, ancak 1/w olasılığıyla gerçekleşir. Bu nedenle, add(x) işleminin beklenen çalışma zamanı olur. sil(x) yöntemi ekle(x) tarafından gerçekleştirilen işleri geri alır. xft.bul(x) cevabını bulmak üzere, xft içinde u, yaprağını buluruz. u’dan, x’i içeren treap, t’yi elde ederiz, ve t’den x’i sileriz. x, xft’de de saklanmışsa (x, 2w – 1’e eşit değilse) o zaman xft’den x’i sileriz ve x'in treap’inin içindeki elemanları, bağlantılı listede u’nun ardından gelen, t2, treap’inin içine ekleriz. Şekil 13.9’da bu gösterilmiştir. 278 xft içinde u düğümünü bulma O(log w) beklenen zaman alır. t’den x silme O(log w) beklenen zaman alır. Yine, Alıştırma 7.12, t içindeki tüm elemanları t2 içinde birleştirmenin O(log w) zamanda yapılabilir olduğunu göstermektedir. Gerekirse, xft’den x’i silme O(w) zaman alır, ancak x yalnızca 1/w olasılığı ile xft’de yer almaktadır. Bu nedenle, Y-Hızlı Sıralı Ağaç’tan bir eleman silmek için beklenen zaman O(log w) olur. Daha önceki tartışmamızda, bu yapının içindeki treap boyutları konusunu bu zamana kadar erteledik. Bu bölümü bitirmeden önce, ihtiyaç duyduğumuz sonucu kanıtlayacağız. 279 Önerme 13.1. x, Y-Hızlı Sıralı Ağaç’ta depolanan bir tamsayı olsun ve x’i içeren treap, t’nin eleman sayısı nx ile göstersin. O zaman E[nx] ≤ 2w – 1 olur. Kanıt. Şekil 13.10’a bakın. x1 < x2 < … < xi = x < xi+1 < …< xn Y-Hızlı Sıralı Ağaç’ta depolanan elemanları ifade etsin. Treap t, x’e eşit veya daha büyük bazı elemanları içeriyor. Bu elemanlar xi , xi+1, …, xi+j-1 ise, xi+j-1 ekle(x) yönteminde fırlatılan hileli paranın tura olarak döndüğü tek elemandır. Diğer bir deyişle, E[j], ilk turayı elde etmek için gereken hileli para fırlatmanın beklenen sayısına eşittir23. Her yazı tura atma bağımsızdır ve tura gelme olasılığı 1/w olarak ortaya çıkıyor, bu nedenle, E[j] ≤ w. (w = 2 için, bu durumun bir analizi için Önerme 4.2'ye bakın.) Benzer şekilde, x’den daha küçük t elemanları xi–1, …, xi–k ile veriliyor. Burada ilk k para fırlatma için yazı ve kalan xi–k–1 para fırlatmanın sonucu tura gelir. Bu önceki paragrafta ele alınan aynı yazı tura atma deneyi olduğu için, ancak son para fırlatmanın sayılmadığı için bu E[k] ≤ w – 1 olur. Özetle, nx = j + k, bu nedenle Önerme 13.1. Y-Hızlı Sıralı Ağaç performansını özetleyen aşağıdaki teoremin kanıtının son parçasıydı: Teorem 13.3. Y-Hızlı Sıralı Ağaç w-bit tamsayılar için Sıralı Küme arayüzünü uygular. Y-Hızlı Sıralı Ağaç ekle(x), sil(x) ve bul(x) işlemlerini işlem başına O(log w) beklenen zamanda destekler. n değer içeren Y-Hızlı Sıralı Ağaç’ın kullanması gerekli alan O(n + w) kadardır. 23 Bu analiz j’nin asla n – i + 1 değerini aşmayacağı gerçeğini göz ardı ediyor. Ancak, bu sadece E[j]’yi azaltır, böylece üst sınır hala geçerlidir. 280 Alan gereksinimindeki w terimi xft’nin her zaman değer 2w-1 değeri depoladığı gerçeğini anlatıyor. Uygulamada değişiklikler yapılabilir (koda bazı ekstra durumları ekleme pahasına), böylece bu değeri saklamak gereksiz olabilir. Bu durumda, teoremdeki alan gereksinimi O(n) olur. 13.4. Tartışma ve Alıştırmalar ekle(x), sil(x) ve bul(x) işlemlerini O(log w) zamanda gerçekleştiren ilk veri yapısı van Emde Boas tarafından önerilmişti ve o zamandan beri van Emde Boas (ya da Katmanlı) Ağacı [74] olarak bilinir hale gelmiştir. Orijinal van Emde Boas yapısı 2w büyüklüğündeydi, büyük tamsayılar için bu pratik değildi. X-Hızlı Sıralı Ağaç ve Y-Hızlı Sıralı Ağaç veri yapıları Willard [77] tarafından keşfedildi. van Emde Boas ağaçları ile X-Hızlı Sıralı Ağaç yapısı yakından ilgilidir; örneğin, bir X-Hızlı Sıralı Ağaç’taki karma tabloları van Emde Boas ağacındaki diziler ile yer değiştirmiştir. Bunun anlamı, karma tablosu t[i] saklamak yerine, van Emde Boas ağacı 2i uzunluğunda bir diziyi saklar. Tamsayıları saklamak için başka bir yapı da Fredman'in ve Willard'ın füzyon ağaçlarıdır [32]. Bu yapı, n adet w-bit tamsayıları O(n) büyüklüğündeki bir alanda saklayabilir, böylece bul(x) işlemi O((log n / (log w)) zamanda çalışır. kullanarak ve olduğunda bir füzyon ağacını olduğunda bir Y-Hızlı Sıralı Ağaç kullanarak O(n) alana gereksinimi olan ve bul(x) işlemini zamanda uygulayabilen bir veri yapısı elde edilir. son zamanlarda Patrascu ve Thorup’un alt sınır sonuçları [59] göstermektedir ki, bu sonuçlar, en azından sadece O(n) alan kullanan yapılar için az veya çok en iyidir. Alıştırma 13.1. Bağlantılı liste yada jump işaretçilerini içermeyen basitleştirilmiş bir İkili Sıralı Ağaç versiyonunu tasarlayın ve uygulayın. bul(x) çalışma zamanı O(w) olmalıdır. 281 Alıştırma 13.2. Hiçbir İkili Sıralı Ağaç kullanmayan basitleştirilmiş bir X-Hızlı Sıralı Ağaç versiyonunu tasarlayın ve uygulayın. Uygulamanız, bunun yerine, bir Çifte Bağlantılı Liste’de ve w + 1 karma tabloda her şeyi saklamalıdır. Alıştırma 13.3. İkili Sıralı Ağaç’ı her bit dizisinin kökten-yaprağa bir yol temsil ettiği w uzunluğunda bit dizelerini depolayan bir yapı olarak düşünebiliriz. Değişken uzunluklu dizeleri depolayan ve s uzunluğuna orantılı zamanında ekle(x), sil(x) ve bul(x) işlemlerini gerçekleştiren bir Sıralı Küme uygulaması halinde bu fikri genişletin. İpucu: Veri yapınızdaki her düğüm, karakter değerleri ile endeksli bir karma tablo saklamalıdır. Alıştırma 13.4. Bir tamsayı x ∈ {0, …, 2w – 1} için, d(x), x ile find(x) tarafından döndürülen değeri arasındaki farkı göstersin [find(x) null döndürürse, o zaman d(x) = 2w olarak tanımlanır]. Örneğin, bul(23) 43 döndürürse, o zaman d(23) = 20. 1. O(1 + log d(x)) beklenen zamanda çalışan bul(x) işleminin değiştirilmiş bir sürümünü X-Hızlı Sıralı Ağaç için tasarlayın ve uygulayın. İpucu: t[w] karma tablosu öyle x değerlerini içerir ki, d(x) = 0, buradan başlamak iyi bir nokta olacaktır. 2. O(1 + log log d(x)) beklenen zamanda çalışan bul(x) işleminin değiştirilmiş bir sürümünü X-Hızlı Sıralı Ağaç için tasarlayın ve uygulayın. 282 Bölüm 14 Dış Bellek Aramaları Bu kitap boyunca, Bölüm 1.4’de tanımlandığı gibi w-bit sözcük-RAM hesaplama modelini kullanıyorduk. Bu modelin olağan varsayımı, bilgisayarın veri yapısı içindeki tüm verileri depolamak için yeterince büyük bir rasgele erişim belleği olmasıdır. Bazı durumlarda, bu varsayım geçerli değildir. Hiçbir bilgisayarın bunları saklamak için yeterli bellek olmadığı büyük veri koleksiyonları vardır. Bu gibi durumlarda uygulamanın bir sabit disk, bir katı hal diski, hatta bir ağ dosya sunucusu (kendi harici depolaması olan) gibi herhangi bir dış depolama ortamı üzerinde veri depolamaya başvurması gerekir. Harici depolamadan bir elemana erişmek son derece yavaştır. Bu kitabın yazıldığı bilgisayara bağlı sabit diskin 19 ms. ve katı hal sürücünün 0,3 ms. ortalama erişim süresi vardır. Bunun aksine, bilgisayardaki rasgele erişim belleğinin 0,000113 ms.’den daha düşük bir ortalama erişim süresi vardır. RAM erişimi, katı hal sürücüsüne erişimden 2.500 kat daha fazla hızlıdır ve sabit sürücüye erişimden 160.000 kat daha fazla hızlıdır. Bu hızlar oldukça tipiktir; rasgele bir bayta RAM’dan erişmek bir sabit diskten veya katı hal sürücüden rasgele bayta erişmekten binlerce kez daha hızlıdır. Ancak sadece erişim süresi, bütün hikayeyi anlatmıyor. Bir sabit disk veya katı hal diskinden bir bayta eriştiğinizde, diskin bütün bir bloğu okunur. Bilgisayara bağlı sürücülerin her birinin 4.096 blok boyutu vardır; biz bir bayt okuduğumuzda, sürücü bize her zaman 4.096 bayt içeren bir blok verir. Dikkatli bir şekilde veri yapısını düzenlersek, bu her disk erişiminin yaptığımız işi tamamlamak için yararlı olabilecek 4,096 bayt verdiği anlamına gelir. Şekil 14.1’de şematik olarak gösterilen dış bellek modeli hesaplamasının arkasındaki fikir budur. Bu modelde, bilgisayarın, tüm verilerin bulunduğu büyük bir dış belleğe erişimi vardır. Bu bellek her biri B sözcük içeren bellek bloklarına ayrılmıştır. Bilgisayar aynı zamanda hesaplamaları gerçekleştirmek için sınırlı bir iç belleğe de sahiptir. İç bellek ve dış bellek arasında bir blok aktarma sabit zaman alır. İç bellek içinde yapılan hesaplamalar bedavadır, 283 hiçbir zaman gerektirmezler. İç hafıza hesaplamalarının bedava olması aslında biraz garip gelebilir, ama sadece dış belleğin RAM’dan çok daha yavaş olduğu gerçeğini vurgulamaktadır. Tam gelişmiş dış bellek modelinde, iç bellek boyutu da bir parametredir. Ancak, bu bölümde açıklanan veri yapıları için, O(B + logB n) boyutunda bir iç belleğe sahip olmak yeterlidir. Yani, belleğin sabit bir sayıda blokları ve O(logB n) yüksekliğinde bir özyineleme yığınını saklama yeteneğine sahip olması gerekir. Çoğu durumda, bellek gereksinimi için O(B) yeterlidir. Örneğin hatta nispeten küçük bir B=32 değeri ve her n ≤ 2160 için, B ≥ logB n olur. Onluk tabanda, her için B ≥ logB n olur. 14.1 Blok Deposu Dış bellek kavramı çok sayıda olası farklı cihazları içerir; bunların her biri kendine ait bir blok boyutuna sahiptir ve kendi koleksiyonlarına ait sistem çağrıları ile erişilir. Ortak fikirler 284 üzerinde odaklanmak üzere bu bölümün anlaşılmasını basitleştirmek için BlokDeposu adında bir nesne ile dış bellek cihazlarını kılıflıyoruz. BlokDeposu, her biri B boyutunda olan bellek bloklarının bir koleksiyonunu saklar. Her blok benzersiz tamsayı endeksi ile tanımlanır. Bir BlokDeposu şu işlemleri destekler: 1. readBlock(i): i endeksli bloğun içeriğini döndürür. 2. writeBlock(i, b): i endeksli bloğun içeriğini b olarak yazar. 3. placeBlock(b): Yeni bir endeks döndürür ve bu endeksin içeriğini b olarak depolar. 4. freeBlock(i): i endeksli bloğu serbest bırakır. Bu bloğun içeriğinin artık kullanılmadığı anlamına gelmektedir, bu nedenle bu blok tarafından ayrılan dış bellek yeniden kullanılabilir. BlokDeposu’nu düşünmenin en kolay yolu disk üzerinde her biri B bayt içeren bloklara bölünen bir dosyayı depolamanın tasarımlanmasıdır. Bu şekilde, readBlock(i) ve writeBlock(i, b) bu dosyadan sadece iB, ..., (i + 1)B – 1 bayt okur ve üzerine yazar. Buna ek olarak basit bir BlokDeposu, kullanıma hazır olan blokların bir serbest listesini tutabilir. freeBlock(i) tarafından serbest bırakılan bloklar serbest listesine eklenir. Bu şekilde, placeBlock(b) serbest listeden bir blok kullanabilir yada hiçbiri hazır kullanılışlı değilse, dosyanın sonuna yeni bir blok ekler. 14.2 B-Ağaçları Bu bölümde, dış bellek modelinde verimliliği olan ve B-ağaç’ları denilen, İkili Ağaç’ların bir genellemesini tartışıyoruz. Alternatif olarak, B-ağaç’ları, Bölüm 9.1’de anlatılan 2-4 Ağaç’larının doğal genellemesi olarak görülebilir. (2-4 Ağaç, B = 2 olarak ayarlanmış B-ağaç’ının özel bir durumudur.) Herhangi bir tamsayı B ≥ 2 için, B-ağaç’ı yaprakların aynı derinliğe sahip olduğu ve kökolmayan her, u, iç düğümünün en az B çocuğu ve en çok 2B çocuğunun olduğu bir ağaçtır. u çocukları bir u.children dizisinde depolanır. Gerekli çocuk sayısı kökte dikkate alınmaz, 2 ve 2B aralığında herhangi bir sayıda olabilir. 285 Bir B-Ağaç’ının yüksekliği h ise, o zaman B-Ağaç’ının yaprak sayısı, l , için şu söylenebilir: İlk eşitsizliğin logaritması alınarak ve terimler yeniden düzenlerek, elde edilir. Yani, bir B-ağaç’ın yüksekliği yaprak sayısının B-tabanında logaritması ile orantılıdır. B-ağaç’taki her, u, düğümü bir dizi u.keys[0], …, u.keys[2B – 1] pozisyonlarında anahtar depolar. u, k çocuklu bir iç düğüm ise, o zaman u tarafından depolanan anahtarların sayısı tam olarak k – 1 olur ve bunlar u.keys[0], ..., u.keys[k – 2] pozisyonlarında saklanır. u.keys dizisinin kalan 2B - k + 1 girişleri null olarak ayarlanır. u kök olmayan bir yaprak düğüm ise o zaman, u, B – 1 ve 2B – 1 arasında bir anahtar değer saklar. B-ağaç’ın anahtarları İkili Arama Ağaç’ının anahtarlarının benzeridir. k – 1 anahtar depolayan her, u, düğümü için, sıralaması geçerlidir. Eğer, u, bir iç düğüm ise, o zaman her i ∈ {0, …, k – 2} için u.keys[i], u.children[i] kökenli altağaçta saklanan her anahtardan daha büyüktür, ancak u.children[i+1] kökenli altağaçta saklanan her anahtardan daha küçüktür. Yani, B=2 olan B-ağaç’ına bir örnek Şekil 14.2’de gösterilmektedir. 286 B-ağaç’ın bir düğümünde saklanan veri boyutunun O(B) olduğuna dikkat edin. Bu nedenle, dış bellek ortamında, B-ağaç’ı içindeki B değeri, bir düğüm bir adet dış bellek bloğuna sığacak şekilde seçilmelidir. Bu şekilde, dış bellek modelinde bir B-ağaç işlemini gerçekleştirmek için gereken zaman işlem sırasında erişilen (okunan veya yazılan) düğüm sayısı ile orantılıdır. Örneğin, anahtarlar 4 bayt tamsayı ve düğüm işaretleri de 4 bayt ise, o zaman B = 256 ayarlamasıyla her düğümde bayt veri saklanır. Blok boyutu 4096 bayt olan sabit disk veya bu bölümün başında tartışılan katı hal sürücüsü için bu mükemmel bir B değeri olacaktır. B-ağaç’ı ri, uygulayan BAğaç sınıfı, B-ağaç’ın düğümlerinin yanı sıra, kök düğümün endeksini, saklayan bir BlokDeposu’nu, bs, saklar. Her zamanki gibi, veri yapısındaki eleman sayısını takip etmek için, n tamsayısı, kullanılır: 287 14.2.1 Arama Şekil 14.3’te gösterilen bul(x) işleminin uygulanması, İkili Arama Ağacı’ndaki bul(x) işleminin genelleştirilmesidir. x için arama kökte başlar ve bir u düğümünün çocuklarının hangisinden aramanın devam edeceğini belirlemek için u düğümünde depolanan anahtarları kullanır. Daha özel olarak ise, bir u düğümünde, arama x’in u.keys içinde saklı olduğunu kontrol eder. Eğer öyleyse, x bulunmuştur ve arama tamamlanır. Aksi takdirde, arama u.keys [i]> x olan en küçük, i, tamsayısını bulur, ve u.children[i] kökenli altağaçta arama devam eder. u.keys’teki hiçbir anahtar x’den büyük değilse, o zaman arama u'nun en sağdaki çocuğundan devam eder. Aynı ikili arama ağaçlarında olduğu gibi, algoritma x’den büyük olan en son 288 görülen, z, anahtarını izler. x bulunmazsa, x’e eşit yada daha fazla olan en küçük değer olarak, z, döndürülür. bul(x) yönteminin merkezinde, null ile doldurulmuş sıralı bir, a, dizisi içinde x değerini arayan findIt(a, x) yöntemi bulunmaktadır. Şekil 14.4’de gösterilen bu yöntem, sıralı olarak dizilmiş anahtarların a[0], ..., a[k - 1] tarafından temsil edildiği herhangi bir, a, dizisi için ve a[k], ..., a[a.length – 1] tümü null olarak ayarlandığında çalışır. Eğer x, dizide, i pozisyonunda ise, o zaman findIt(a, x) –i – 1 döndürür. Aksi takdirde, a[i] > x veya a[i] = null olan en küçük, i, endeksini döndürür. findIt(a, x) yöntemi her adımda arama alanını yarıya bölen ikili arama kullanır, bu nedenle O(log (a.length )) zamanda çalışır. Bizim ortamımızda, a.length = 2B olduğu için, findIt(a, x) çalışma zamanı O(log B) olur. 289 B-ağaç bul(x) işleminin çalışma süresini, hem normal sözcük-RAM modelinde (her komutu hesaba kattığımız) ve hem de dış bellek modelinde (sadece erişilen düğüm sayısını önemsediğimiz) analiz edebiliriz. B-ağaç içindeki her yaprak en az bir anahtar sakladığı için ve l yapraklı B-ağaç’ın yüksekliği O(logBl ) olduğu için, n anahtar saklayan bir B-ağaç’ın yüksekliği O(logB n) olur. Bu nedenle, dış bellek modelinde, bul(x) işleminin aldığı zaman O(logB n) olur. Sözcük-RAM modelinde çalışma süresini belirlerken, eriştiğimiz her düğüm için, findIt(a, x) çağrı maliyetini hesaba katmalıyız, böylece sözcük-RAM modelinde bul(x) işleminin çalışma zamanı olur. 14.2.2 Ekleme B-ağaç’ları ve Bölüm 6.2’de anlatılan İkili Arama Ağacı veri yapısı arasındaki önemli farklardan biri B-ağaç düğümlerinin menşeilerine işaretçi tutmamalarıdır. Bunun nedeni, kısaca açıklanacaktır. Menşei işaretçilerinin eksikliği B-ağaç’lar üzerinde ekle(x) ve sil(x) işlemlerinin en kolay özyineleme kullanılarak uygulanması anlamına gelir. Tüm dengeli arama ağaçları gibi, ekle(x) çalışması sırasında bir tür dengeleme gereklidir. Bir B-ağaç’ında bu, düğümlerin bölünmesi tarafından yapılır. Şekil 14.5’e bakın. Bölünme iki düzeyli özyineleme ortasında yer almasına rağmen, 2B anahtar içeren ve 2B + 1 çocuk sahibi bir, u, düğümünü alan bir işlem olarak en iyi anlaşılmaktadır. u.children[B], ..., u.children[2B] w, u'nun benimsenerek yeni bir, w, düğümü oluşturulur. Yeni düğüm en büyük anahtarlarını u.keys[B], …, u.keys[2B – 1] alır. Bu noktada, u, B çocuğa ve B anahtara sahiptir. Ekstra anahtar u.keys[B – 1], u menşeine kadar geçirilerek w benimsenir. Dikkat etmelisiniz ki, bölme işlemi üç düğüm değiştirir: u, u’nun menşei ve yeni düğüm w. B-ağaç düğümlerinin menşei işaretçileri tutmamasının önemli nedeni budur. Sağlasaydı, o zaman w tarafından benimsenen B + 1 çocuğun tüm menşei işaretçilerinin değiştirilmiş 290 olması gerekirdi. Bu dış bellek erişimlerin sayısını 3’ten B+4’e artıracaktı ve büyük B değerleri için B-ağaç’ları çok daha az etkili olacaktı. B-ağaç ekle(x) yöntemi Şekil 14.6’da gösterilmiştir. Yüksek seviyede bu yöntem, x değerini eklemek için bir, u, yaprağını bulur. Bunun sonucunda B – 1 anahtar içerdiği için, u, fazla doluyken, u, bölünür. u'nun menşei fazla doluluk içindeyse, o zaman u menşei de bölünür, bu u’nun büyükbabasının fazla doluluk içermesine neden olabilir, ve benzeri. Bu süreç ağaç her seferinde bir seviye yukarı hareket ederek fazla dolu olmayan bir düğüme ulaşana kadar veya kök bölünene kadar devam eder. İlk durumda, işlem durur. Sonraki durumda, iki çocuğu orijinal kök bölündüğü zaman elde edilen düğümler haline gelen yeni bir kök oluşturulur. 291 ekle(x) yönteminin kısa özeti, kökten yaprağa x aranır, bulunan yaprağa x eklenir, ve sonra köke dönüş yolunda karşılaşılan herhangi fazla dolu düğümler bölünür. Geliştirdiğimiz bu üst düzey düşünceyle, bu yöntemin özyinelemeli olarak nasıl uygulanabilir olduğunu araştırmayla devam ediyoruz. ekle(x) için gerekli gerçek iş ui tanımlayıcısına sahip u kökenli altağaca x değerini ekleyen, addRecursive(x, ui) yöntemi tarafından yapılıyor. u bir yaprak ise, o zaman x sadece u.keys içine eklenir. Aksi takdirde, özyinelemeli olarak x, uygun u’ çocuğu içine eklenir. Bu özyinelemeli çağrının sonucu normalde null iken, aynı zamanda u’ bölündüğü için yeni oluşturulan, w, düğümüne bir referans olabilir. Bu durumda, w, u tarafından benimsenir ve ilk anahtarı alarak u’ üzerindeki bölme işlemini tamamlar. x değeri eklendikten sonra, (u veya altındaki düğümlerden biri içine) addRecursive(x, ui) yöntemi u tarafından çok fazla anahtar depolandığını (anahtar sayısı en fazla 2B – 1 olmalı) 292 kontrol eder. Eğer öyleyse, u.split() yöntemine yapılan bir çağrı ile u bölünür. u.split() çağrısının sonucu, addRecursive(x, ui) için dönüş değeri olarak kullanılan yeni bir düğümdür. addRecursive(x, ui) yöntemi, B-ağaç’ının köküne bir, x, değeri eklemek için addRecursive(x, ri) çağrısının yapıldığı ekle(x) yöntemini çalıştırır. addRecursive(x, ri) 293 kökü bölene kadar giderse, o zaman hem eski kökü ve hem de eski kökün bölünmesiyle oluşturulan yeni düğümü çocukları olarak alan yeni bir kök oluşturulur. ekle(x) ve addRecursive(x, ui) yöntemlerini iki aşamada analiz edebiliriz: Aşağı yönde: Özyinelemenin aşağı aşamasında, x eklenmeden önce, bir dizi BAğaç’ı düğümüne erişim sağlanır ve her düğüm üzerinde findIt(a, x) çağrısı gerçekleştirilir. bul(x) yönteminde olduğu gibi, bu dış bellek modelinde O(logB n) zaman alır ve sözcük- RAM modelinde O(log n) zamanda çalışır. Yukarı yönde: Özyinelemenin yukarı aşamasında, x eklendikten sonra, bu yöntemler, en fazla bir dizi O(logB n) bölünme gerçekleştirir. Her bölünme sadece üç düğüm içerir, bu nedenle bu aşama dış bellek modelinde O(logB n) zaman alır. Ancak, her bölünme B anahtarın ve çocuklarının bir düğümden diğer düğüme hareket etmesini içerir, böylece bu sözcük-RAM modelinde, O(B log n) zaman alır. B değerinin oldukça büyük, hatta log n’den çok daha büyük olabildiğini hatırlayın. Bu nedenle, sözcük-RAM modelinde, B-ağaç’a bir değer eklemek dengeli İkili Arama Ağacı içine eklemekten çok daha yavaş olabilir. Daha sonra Bölüm 14.2.4’te, durumun o kadar da kötü olmadığını göstereceğiz; bir ekle(x) çalışması sırasında yapılan bölünme işlemlerinin amortize sayısı sabittir. Bu sözcük-RAM modelinde ekle(x) işleminin (amortize) çalışma zamanının O(B + log n) olduğunu gösterir. 14.2.3 Silme B-ağaç içindeki sil(x) işlemi, yine, en kolay bir özyinelemeli yöntemi olarak uygulanmaktadır. sil(x) işleminin özyinelemeli uygulaması, çeşitli yöntemler arasında karmaşıklığı yaymasına rağmen, Şekil 14.7’de gösterilen genel süreç, oldukça kolaydır. Anahtarların karıştırılıp değiştirilmesiyle silme işlemi, bir, x’, değerinin bazı, u, yaprağından çıkarılması problemine indirgenir. x’ çıkarılmasıyla, u, B – 1 anahtar ile bırakılabilir, bu durum alttaşma olarak adlandırılır. 294 Bir alttaşma oluştuğunda, u ya kardeşinden anahtar ödünç alır, veya kardeşlerinden biri ile birleşir. u bir kardeş ile birleştiyse, o zaman, u menşei, bir çocuk daha az ve bir anahtar daha az sahibi olacaktır, bu u’nun menşeinin de alttaşmasına neden olabilir, bu tekrar, ödünç alma veya birleştirerek düzeltilir, ancak birleştirme u'nun büyükbabasının da alttaşmasına neden olabilir. Bu süreç köke kadar artık alttaşma kalmayana kadar veya kökün son iki çocuğu bir tek çocuk halinde birleştirilene kadar geri dönüş yolunda çalışır. İkinci durum olduğunda, kök kaldırılır ve yalnız olan tek çocuk yeni kök olur. Şimdi, bu adımların her birinin nasıl uygulandığının detaylarını araştıracağız. sil(x) yönteminin ilk işi, çıkarılacak x elemanını bulmaktır. x bir yaprakta bulunursa, o zaman bu yapraktan x kaldırılır. Aksi takdirde, x, eğer bazı iç düğüm, u, için u.keys[i] pozisyonunda bulunursa, o zaman algoritma u.children[i + 1] kökenli altağaçtaki en küçük, x’, değerini 295 kaldırır. x’ değeri BAğaç’ta saklanan x’den daha büyük olan en küçük değerdir. x’ değeri daha sonra u.keys[i] içindeki x değerini değiştirmek için kullanılır. Bu süreç, Şekil 14.8’de gösterilmiştir. removeRecursive(x, ui) yöntemi önceki algoritmanın özyinelemeli bir uygulamasıdır: 296 Unutmayın ki, özyinelemeli olarak u’nun i'nci çocuğundan x değerini çıkardıktan sonra, bu çocuğun hala en az B – 1 anahtarı bulunduğunu removeRecursive(x, ui) yönteminin sağlaması gerekmektedir. Önceki kodda, u’nun i'nci çocuğunda bir alttaşma olup olmadığını kontrol eden ve bunu düzelten checkUnderflow(x, i) adı verilen bir yöntem kullanılarak bu yapılır. u’nun i’nci çocuğu w olsun. w sadece B – 2 anahtara sahipse, o zaman bunun düzeltilmesi gerekir. Düzeltme w’nin bir kardeşinin kullanılmasını gerektirir. Bu u’nun ya (i + 1)’nci çocuğudur, yada (i – 1)’nci çocuğudur. Genellikle w’nin doğrudan solundaki kardeşi, v, olan (i – 1)’nci çocuğu kullanacağız. Bunun işe yaramadığı sadece bir zaman vardır, i = 0 iken, bu durumda w’nin doğrudan sağındaki kardeşi kullanırız. Aşağıda, i ≠ 0 durumuna odaklanıyoruz. Burada u’nun i'nci çocuğundaki herhangi bir alttaşma u’nun (i – 1)’nci çocuğu yardımı ile düzeltiliyor. i = 0 durumu da benzerdir ve ayrıntıları ekteki kaynak kodunda bulunabilir. 297 w düğümündeki bir alttaşmayı düzeltmek için, w için daha fazla anahtar (ve muhtemelen de çocuklar) bulmamız gerekiyor. Bunu yapmanın iki yolu vardır: Ödünç alma: w’nin, B – 1 anahtardan daha fazlasına sahip olan, v, adında bir kardeşi varsa, o zaman w bazı anahtarları (ve muhtemelen de çocukları) v’den ödünç alabilir. Daha spesifik olarak, v, size(v) tane anahtar depoluyorsa o zaman, v ve w, aralarında anahtara sahip olur. Bu nedenle, v’den w’ye anahtarları kaydırabiliriz, böylece v ve w’nin her biri en az B – 1 anahtara sahip olacaktır. Bu süreç Şekil 14.9’da gösterilmiştir. 298 Birleştirme: Eğer v’nin sadece B – 1 anahtarı varsa, daha zorlayıcı bir şey yapmamız gerekir, çünkü v herhangi bir anahtarı w’ye vermeyi göze alamaz. Bu nedenle, Şekil 14.10’de gösterildiği gibi v ve w birleştirilir. Birleştirme işlemi bölme işleminin tersidir. 2B – 3 toplam anahtar içeren iki düğüm alır ve onları 2B – 2 anahtar içeren tek bir düğüm haline birleştirir. (Ek anahtarın gelmesinin nedeni şudur: v ve w’yi birleştirdiğimizde, onların ortak menşei u’nun şimdi bir daha az çocuğu vardır ve dolayısıyla anahtarlarından birisinden vazgeçmesi gerekiyor.) Özetlemek gerekirse, B-ağaç’taki sil(x) yöntemi kökten yaprağa bir yol izler, u, yaprağından bir x’ anahtarını kaldırır, ve sonra u ve onun atalarını içeren sıfır veya daha fazla birleştirme işlemleri gerçekleştirir, ve en fazla bir ödünç alma işlemi gerçekleştirir. Her birleştirme ve ödünç alma işlemi sadece üç düğüm değiştirmeyi içerdiğinden, ve bu işlemlerin sadece 299 O(logB n) kadarı oluştuğundan, dış bellek modelinde tüm süreç O(logB n) çalışma zamanı alır. Yine, her birleştirme ve ödünç alma işlemi sözcük-RAM modelinde O(B) zaman almaktadır, bu yüzden (şimdilik) sözcük-RAM modelinde sil(x) tarafından gerekli çalışma süresi hakkında söyleyebileceğimiz en fazla şey O(B logB n) olduğudur. 14.2.4 B-Ağaç’ların Amortize Analizi Şimdiye kadar, göstermiştik ki: 1. Dış bellek modelinde, B-ağaç’ın bul(x), ekle(x) ve sil(x) işlemlerinin çalışma zamanı O(logB n) olur. 2. Sözcük-RAM modelinde, bul(x) işleminin çalışma zamanı O(log n) ve, ekle(x) ve sil(x) işlemlerinin çalışma zamanı O(B log n) olur. 300 Aşağıdaki önerme gösteriyor ki, şimdiye kadar, B-ağaç’ları tarafından gerçekleştirilen birleştirme ve bölünme işlemlerinin sayısını olduğundan fazla tahmin ettik. Önerme 14.1. Boş bir B-ağaç ile başlayarak herhangi bir sırada m adet ekle(x) ve sil(x) işlemlerini yürütürsek en çok 3m/2 kadar bölünme, birleştirme, ve ödünç alma gerçekleştirilir. Kanıt. Bunun kanıtı zaten Bölüm 9.3’te, B = 2 özel durumu için, kısaca anlatılmıştı. Önerme kredi düzeni kullanılarak kanıtlanabilir. Burada, 1. her bölme, birleştirme, ya da ödünç alma işlemi iki kredi ile ödenir, yani, bu işlemlerin biri oluştuğunda her zaman bir kredi kaldırılır; ve 2. herhangi ekle(x) veya sil(x) işlemi sırasında en çok üç kredi oluşturulur. Herhangi bir zamanda en çok 3m kredi oluşturulduğu için ve her bir bölünme, birleştirme ve ödünç alma iki kredi ile ödendiği için en çok 3m/2 bölünme, birleştirme, ve ödünç alma yapıldığı sonucuna varırız. Bu krediler Şekil 14.5, 14.9 ve 14.10 olarak ¢ sembolü kullanılarak gösterilmektedir. Bu kredileri takip etmek için kanıt aşağıdaki kredi değişmeyenini korur: B – 1 anahtara sahip kök-olmayan bir düğüm bir kredi depolar ve 2B-1 anahtarlı herhangi bir düğüm üç kredi saklar. En az B anahtar ve en çok 2B – 2 anahtar depolayan bir düğümün herhangi bir kredi saklaması gerekmez. Geriye kalan yapmamız gereken, kredi değişmeyenini korumak olduğunu göstermek ve her ekle(x) ve sil(x) işlemi sırasında, yukarıda verilen, 1. ve 2. özellikleri sağlamaktır. Ekleme: ekle(x) yöntemi herhangi bir birleştirme veya ödünç alma gerçekleştirmez, bu nedenle sadece ekle(x) için yapılan aramaların bir sonucu olarak ortaya çıkan bölünme işlemlerini dikkate almamız gerekiyor. Her bölünme işlemi gerçekleştiğinde zaten 2B – 1 anahtar içeren bir, u, düğümüne bir anahtar eklenir. Bu durumda, u, B – 1 anahtar içeren u’, ve B anahtar içeren u’’ düğümü olmak üzere iki düğüme ayrılmıştır. Bu işlem öncesinde, u, 2B - 1 anahtar ve dolayısıyla üç kredi saklıyordu. Bu kredilerin ikisi bölünmeyi ödemek için kullanılabilir ve diğer kredi, kredi 301 değişmeyenini korumak için (B – 1 anahtara sahip olan) u’ düğümüne verilebilir. Bu nedenle, bölünme için ödeme yapabiliriz ve herhangi bir bölünme sırasında kredi değişmeyenini koruyabiliriz. Bir ekle(x) işlemi sırasında meydana gelen düğümler için sadece yapılan diğer değişiklik, bütün bölünmeler, varsa, tamamlandığında olur. Bu değişiklik, bazı u’ düğümüne yeni bir anahtar eklenmesini içerir. Eğer, bunun öncesinde u’ düğümünün 2B – 2 çocuğu vardıysa, şimdi 2B – 1 çocuk sahibidir ve bu nedenle üç kredi almak zorundadır. Bu ekle(x) yöntemi ile dağıtılan yalnız ve tek kredidir. Silme: sil(x) için yapılan bir çağrı sırasında sıfır veya daha fazla birleştirme meydana gelir ve muhtemelen bunu tek bir ödünç alma izler. Her birleştirme, sil(x) çağrısı öncesinde her biri tam olarak B – 1 anahtar sahibi iki düğümün, v ve w, tam olarak 2B – 2 anahtara sahip tek bir düğüm haline birleştirilmesi nedeniyle oluşur. Bu türde her birleştirme, bu nedenle, birleştirmeyi ödemek için kullanılabilecek olan iki krediyi serbest bırakır. Herhangi bir birleştirme yapıldıktan sonra, en fazla bir ödünç alma işlemi gerçekleşir, daha sonra başka hiçbir birleştirme yada ödünç alma meydana gelmez. Bu ödünç alma işlemi sadece B – 1 anahtara sahip bir, v, yaprağından bir anahtarı kaldırırsak oluşur. v düğümü bu nedenle bir krediye sahiptir, ve bu kredi ödünç alma maliyetine doğru gider. Bu tek kredi ödünç almayı ödemek için yeterli değildir, bu nedenle ödemeyi tamamlamak için bir kredi yaratırız. Bu noktada, bir kredi yarattık ve hala kredi değişmeyeninin korunabilir olduğunu göstermemiz gerekiyor. En kötü durumda, v’nin, kardeşi, w, ödünç almadan önce tam olarak B anahtara sahiptir, böylece, daha sonra, v ve w, her ikisinin de B – 1 anahtarı olacaktır. Bunun anlamı, işlem tamamlandığında v ve w, her ikisi de kredi depoluyor olması gerekir. Bu nedenle, bu durumda, v ve w’ye vermek için ek iki kredi oluştururuz. Bir sil(x) çalışması sırasında en fazla bir kez ödünç alma olduğu için bu, gerektiği gibi en çok üç kredi yarattığımız anlamına gelir. sil(x) işlemi ödünç alma işlemi içermiyorsa, bunun nedeni, işlemden önce B veya daha fazla anahtarı olan bazı düğümden bir anahtar çıkararak bitirdiği içindir. En kötü durumda, bu 302 düğümün, tam olarak B anahtarı vardı, öyle ki şimdi B – 1 anahtarı bulunuyor ve yarattığımız bir kredi verilmelidir. Her iki durumda da – silme ödünç alma ile işini bitirse de veya bitirmese de – sil(x) için yapılan bir çağrı sırasında kredi değişmeyenini korumak ve meydana gelen tüm ödünç alma ve birleştirmelerin ödemesini yapmak için en fazla üç kredi yaratılmalıdır. Bu önermenin kanıtını tamamlar. Önerme 14.1’in amacı sözcük-RAM modelinde, m adet ekle(x) ve sil(x) işlemi sırasında gerçekleştirilen bölünme ve birleştirmelerin çalışma zamanı sadece O(Bm) olduğunu göstermektir. Yani, işlem başına amortize edilmiş maliyet sadece O(B) olur, böylece sözcükRAM modelinde ekle(x) ve sil(x) amortize maliyeti O(B + log n) olur. Aşağıdaki teorem çifti bunu özetliyor: Teorem 14.1 (Dış Bellek B-Ağaç’ları). BAğaç, Sıralı Küme arayüzünü uygular. Dış Bellek modelinde, BAğaç, işlem başına O(logB n) zamanda çalışan ekle(x), sil(x), ve bul(x) işlemlerini destekler. Teorem 14.2 (Sözcük-RAM B-Ağaç’ları). BAğaç, Sıralı Küme arayüzünü uygular. SözcükRAM modelinde, bölünmeler, birleştirmeler, ve ödünç almaların maliyetini gözardı ederek, BAğaç, işlem başına O(log n) zamanda çalışan ekle(x), sil(x), ve bul(x) işlemlerini destekler. Boş bir BAğaç ile başlayarak herhangi bir sırada m adet ekle(x) ve sil(x) işlemlerini yürütürsek bölünme, birleştirme, ve ödünç almaları gerçekleştirmek için gerekli zaman toplamda O(Bm) olur. 14.3 Tartışma ve Alıştırmalar Hesaplamanın dış bellek modeli Aggarwal ve Vitter tarafından tanıtıldı [4]. Bazen I/O modeli veya disk erişimi modeli de denir. İkili Arama Ağaç’ları içsel bellek araması için ne ise, B-ağaç’lar için de dış bellek araması odur. B-ağaç’ları [9] 1970 yılında Bayer ve McCreight tarafından tanıtıldı ve on yıldan daha 303 az bir süre sonra, Comer ACM Bilgisayar Anketleri makalesi onları her yerde hazır bulunan olarak anmıştır [15]. İkili Arama Ağaç’ları gibi, B-Ağaç’ların B+ ağaçları, B* ağaçları, ve sayılan B-ağaçları dahil olmak üzere birçok çeşidi vardır. B-ağaç’ları, gerçekten her yerde vardır ve Apple'ın HFS+, Microsoft'un NTFS, ve Linux Ext4 dahil olmak üzere birçok dosya sistemlerinde; her büyük veritabanı sisteminde; ve anahtar-değer saklayan bulut bilişimde birincil veri yapısıdır. Graefe’nin en son anketi [36] birçok modern uygulamalar, varyantları, ve B-ağaç’larının optimizasyonları üzerine 200+ sayfa bir bakış sağlar. B-Ağaç’ları Sıralı Küme arayüzünü uygular. Yalnızca Sırasız Küme arayüzü gerekli ise, dış bellek karma yöntemi B-ağaç’larının bir alternatifi olarak kullanılabilir. Dış bellek karma tasarımları vardır; bakınız örneğin, Jensen ve Pagh [43]. Bu tasarımlar Sırasız Küme işlemlerini dış bellek modelinde O(1) beklenen zamanda uygular. Bununla birlikte, çeşitli nedenlerden dolayı, birçok uygulama sadece Sırasız Küme işlemlerini gerektiriyor olsa bile hala B-ağaç’larını kullanmaktadır. B-ağaç’larını böyle popüler bir seçim yapan nedenlerden biri genellikle önerilen O(logB n) çalışma süresi sınırlarından daha iyi performans gösteriyor olmasıdır. Bunun nedeni, dış bellek ortamlarında B değeri genellikle oldukça büyüktür – yüzler hatta binler cinsinden. Bunun anlamı, B-ağaç’ı verilerinin % 99, hatta % 99.9 kadarı yapraklarda depolanır. Büyük bir belleğe sahip bir veritabanı sisteminde, bir B-ağaç’ın RAM’deki tüm iç düğümlerini önbelleğe almak mümkün olabilir, çünkü toplam veri setinin sadece %1 veya %0.1 kadarını temsil ederler. Bu durumda, bunun anlamı bir B-ağaç RAM içinde iç düğümler aracılığıyla, ve bunu takiben bir yaprak bulup getirmek için tek bir dış bellek erişimiyle çok hızlı bir arama gerçekleştirebilir. Alıştırma 14.1. 1,5 ve 7,5 anahtarlarının Şekil 14.2’deki B-ağaç’ına eklendiğinde ne olduğunu gösterin. Alıştırma 14.2. 3 ve 4 anahtarlarının Şekil 14.2’deki B-ağaç’ından silindiğinde ne olduğunu gösterin. 304 Alıştırma 14.3. n anahtar saklayan bir B-ağaç’ının iç düğümlerinin maksimum sayısı nedir? (n ve B fonksiyonu olarak) Alıştırma 14.4. Bu bölümün giriş kısmında B-ağaç’larının sadece O(B + logB n) boyutunda içsel belleğe ihtiyaçları olduğu öne sürülmüştü. Ancak, burada verilen uygulama gerçekte daha fazla bellek gerektirir. 1. Bu bölümde verilen ekle(x) ve sil(x) yöntemlerinin uygulamasının O(B logB n) ile orantılı içsel bellek kullandığını gösterin. 2. Bu yöntemlerin bellek tüketimini O(B + logB n) düzeyine azaltmak amacıyla nasıl değiştirilebileceklerini açıklayın. Alıştırma 14.5. Önerme 14.1’in Şekil 14.6 ve 14.7’deki ağaçlar üzerindeki kanıtında kullanılan kredileri yazın. bölünme, birleştirme, ödünç alma ve kredi değişmeyenini korumanın (üç ek kredi ile) ödenmesinin mümkün olduğunu doğrulayın. Alıştırma 14.6. Düğümleri B’den 3B’ye kadar çocuk (ve dolayısıyla B – 1’den 3B – 1’e kadar sayıda anahtar) sahibi olan B-ağaç’ın değiştirilmiş bir sürümünü tasarlayın. B-ağaç’ların bu yeni versiyonunun, m işlem dizisi sırasında sadece O(m/ B) bölünme, birleştirme ve ödünç alma gerçekleştirdiğini gösterin. Alıştırma 14.7. Bu uygulamada, B-ağaç’larında bölünme ve birleştirmenin değiştirilmiş bir yöntemini tasarlayacaksınız. Bu yöntem, bir anda üç düğüme kadar dikkate alarak, bölünme, ödünç alma ve birleştirme sayısını asimptotik olarak azaltıyor. 1. u fazla dolu bir düğüm ve v, u’nun hemen sağındaki kardeş olsun. u taşmasını düzeltmek için iki yol vardır: a) u anahtarlarından bazılarını v’ye verebilir; veya b) u bölünebilir ve, u ve v anahtarları u, v ve yeni oluşturulan w düğümü arasında eşit olarak dağıtılabilir, 305 Bu işlem sonrasında etkilenen düğümlerin her birinin (en fazla 3 adet) herhangi bir sabit için en az ve en çok anahtar sahibi olduğunu gösterin. 2. u az dolu bir düğüm ve, v ve w, u’nun kardeşleri olsun. u alttaşmasını düzeltmek için iki yol vardır: a) anahtarlar u, v ve w arasında dağıtılabilir; veya b) u, v, ve w iki düğüm halinde birleştirilir ve u, v ve w anahtarları bu düğümler arasında dağıtılabilir. Bu işlem sonrasında etkilenen düğümlerin her birinin (en fazla 3 adet) herhangi bir sabit için en az ve en çok anahtar sahibi olduğunu gösterin. 3. Bu değişiklikler ile, m işlem sırasında gerçekleştirilen birleştirme, ödünç alma, ve bölünme sayısının O(m/B) olduğunu gösterin. Alıştırma 14.8. Şekil 14.11’de gösterilen B+-Ağaç’ı her bir anahtarı bir yaprakta tutar ve yaprakları Çifte-Bağlantılı Liste olarak depolar. Her zamanki gibi, her yaprak B – 1 ve 2B – 1 arası bir sayıda anahtar saklar. Sonuncusu dışında her yaprağın en büyük değerini saklayan standart bir B-ağaç’ı bu listenin yukarısında yer almaktadır. 1. B+-Ağaç için ekle(x), sil(x) ve bul(x) uygulamalarının hızlı bir şekilde nasıl gerçekleştirilebileceğini anlatın. 2. B+-Ağaç içinde x’ten daha büyük ve y’den daha küçük veya eşit tüm değerleri bildiren findRange(x, y) yönteminin verimli olarak nasıl uygulanacağını açıklayın. 306 3. bul(x), ekle(x), sil(x) ve findRange(x, y) işlemlerini gerçekleştiren bir BPlusTree sınıfı uygulayın. 4. B+-Ağaç’ları bazı anahtarları çoğaltırlar çünkü hem B-ağaç’ında ve hem de listede her ikisinde de saklanır. Bu çoğaltmanın, B’nin büyük değerleri için neden çok fazla pahalı olmadığını açıklayın. 307 KAYNAKÇA 308 309 310 311 312 313 314 315