Açık Veri Yapıları – Java ile Yazar: Pat Morin©

advertisement
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 < n2 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 ∈ { i2r/2 ,…,(i+1)2r/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 dw/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 = Vve 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
Download