Bölüm 13: Öncelikli Kuyruk ve Heap
Giriş
Öğrenme Çıktıları
Öncelikli kuyruğu ve maksimum/minimum heap'i tanımlamak; heap'i ikili arama ağacından ayırmak.
Tam ikili ağacı 1 tabanlı dizide saklamak: ata i/2, çocuklar 2i, 2i+1.
maksimum_yigin_ekle ve maksimum_yigin_al yordamlarını adım adım izlemek.
Ekleme ve silmenin neden O(lg n) olduğunu, en iyi durumun Ω(1) olduğunu göstermek.
Sırasız/sıralı dizi ve liste ile heap'i maliyet tablosu üzerinden kıyaslamak.
Aşağıdan yukarı heap kurmanın (heapify) O(n) olduğunu açıklamak; d-ary heap'i tanımak.
Öncelikli Kuyruk
Öncelikli kuyruk: ilk çıkan en öncelikli öğe
Kuyruktan ilk alınacak (silinecek) öğe, geliş sırasına göre değil en yüksek ya da en düşük değere göre belirlenir.
- Maksimum öncelikli kuyruk (max priority queue): önce en yüksek değer ele alınır.
- Minimum öncelikli kuyruk (min priority queue): önce en düşük değer ele alınır.
Öncelikli Kuyruk
Öncelikli kuyruğa neden gereksinim duyarız?
Her kullanıcı sabit ücret ödüyor (3 sayfa da 30 sayfa da aynı). Kazancı en büyük yapmak için kullanım süresi en kısa olana öncelik verilir → minimum öncelikli kuyruk.
İniş isteyen uçaklardan yakıtı en az olana öncelik verilmelidir → minimum öncelikli kuyruk.
Gerçek sistemlerde de böyledir: örneğin işletim sistemi işlemleri (process) öncelikli kuyruğa koyar.
Öncelikli Kuyruk
Soyut veri tipi: öncelikli kuyruk
/* maksimum öncelikli kuyruk arayüzü */ bool bos_mu(void); bool dolu_mu(void); void ekle(eleman item); /* O(lg n) */ bool maks_al(eleman *cikan); /* sil + döndür */ bool maks_bak(eleman *ust); /* silmeden bak */
Öğe, önceliğiyle birlikte yapıya konur.
En öncelikli öğe döndürülür ve yapıdan çıkarılır.
Heap Yapısı
Heap bir dizide tam ikili ağaç olarak durur
Dizinin 0. indisi kullanılmaz; kök yigin[1].
| Düğüm | İndis |
|---|---|
| kök | 1 |
| i'nin atası | i / 2 |
| sol çocuk | 2i |
| sağ çocuk | 2i + 1 |
| son öğe | n |
0 tabanlı dizide: ata (i−1)/2, çocuklar 2i+1, 2i+2.
Heap Yapısı
Heap denince akla tam ikili ağaç gelmeli, arama ağacı değil
- Tam ikili ağaç: tüm düzeyler dolu, son düzey soldan sağa doğru dolu.
- Arama ağacında sol < kök < sağ; heap'te yalnızca ata ≥ çocuk (sol-sağ arasında sıra yok).
Heap Yapısı
Tanım: maksimum heap ve minimum heap
Maksimum heap (max heap): tam ikili ağaçta her düğümün değeri çocuklarından küçük değilse.
Minimum heap (min heap): tam ikili ağaçta her düğümün değeri çocuklarından büyük değilse.
Bu tanımlar maksimum/minimum öncelikli kuyruk kullanabilmek için verilmiştir.
Heap Yapısı
Hızlı kontrol: hangisi maksimum heap?
Her k ≥ 2 için yigin[k/2] ≥ yigin[k] mi? Yalnızca ata–çocuk çiftlerine bakılır; kardeşler ve kuzenler arasında sıra aranmaz.
Cevap
- (a) Evet: 50≥30, 50≥40, 30≥10, 30≥20, 40≥35.
- (b) Hayır: 4. indisteki 35, atası 30'dan büyük.
- (c) Evet: tam ağaç ve her ata ≥ çocuk (1 ile 2'nin 8'den küçük olması önemli değil).
Heap Yapısı
C ile tanımlar: dizi, sayaç ve makrolar
#define ELEMAN_SAYISI 200 /* maksimum öğe + 1 */ #define YIGIN_DOLUMU(n) ((n) == ELEMAN_SAYISI - 1) #define YIGIN_BOSMU(n) (!(n)) typedef struct { int x; /* öncelik (anahtar) */ /* diğer alanlar */ } eleman; eleman yigin[ELEMAN_SAYISI]; /* [0] kullanılmaz */ int n = 0; /* öğe sayısı */
1..199 indisleri kullanılır; n == 199 ise heap doludur.
(n), YIGIN_BOSMU(a+b) gibi çağrılarda öncelik hatasını önler.
eleman yigin[eleman_sayisi - 1] (199 öğe, indis 0..198) diye tanımlamak. Dolu sayılmadan önce n 199'a çıkabilir ve yigin[199] dizinin dışına yazılır; boyut ELEMAN_SAYISI olmalıdır.Ekleme
Maksimum heap'e öğe ekleme: önce yere konur, sonra kaydırılır
Başlangıç: 14, 12, 7, 10, 8, 6. Sırasıyla 15, 109, 107, 3, 15 eklenecek.
Önce yeni yer açılır: tam ağacın ilk boş konumu (7).
15 > 7: ata 7 aşağı kaydırılır, boşluk 3. konuma çıkar.
15 > 14: ata 14 aşağı kaydırılır, boşluk 1. konuma çıkar.
15, 1. konuma yazılır. Heap özelliği yeniden sağlandı.
Ekleme
maksimum_yigin_ekle: takas değil, kaydırma
void maksimum_yigin_ekle(eleman item) { if (YIGIN_DOLUMU(n)) { yigin_dolu(); /* hata iletisi */ return; } int i = ++n; while (i != 1) { /* i == 1: kökteyiz */ if (item.x <= yigin[i / 2].x) break; yigin[i] = yigin[i / 2]; /* atayı aşağı kaydır */ i /= 2; /* bir üst ataya bak */ } yigin[i] = item; }
Takas yok: büyük atalar birer düzey aşağı kaydırılır, öğe en sonda bir kez yazılır (takas 3, kaydırma 1 atama).
i != 1: kökte ata yoktur. item.x <= ata olduğunda yerleşim yeri bulunmuştur.
yigin_dolu() bir hata iletisi yordamıdır; ekleme yapılmadan dönülür.
Ekleme · kodu adım adım çalıştır
Kod izleme: maksimum_yigin_ekle(15)
ekle(15) çağrılır; heap'te 6 öğe var: 14, 12, 7, 10, 8, 6.
YIGIN_DOLUMU(6) yanlış: 6 ≠ 199, yer var.
i = ++n = 7: tam ağacın ilk boş konumu. Yeni öğenin yeri kesinlikle hazır; ama önce atasıyla kıyaslanacak, item henüz yazılmaz.
i = 7 ≠ 1: henüz kökte değiliz, döngüye girilir.
15 ≤ 7? Hayır. Yeni öğe atasından büyük: ata aşağı inmeli.
yigin[7] = yigin[3]: ata (7) bir düzey aşağı kaydı. Takas yok; boşluk (turuncu) yukarı çıktı.
i /= 2 → i = 3: bir üstteki ataya geçilir.
i = 3 ≠ 1: henüz kökte değiliz, döngüye girilir.
15 ≤ 14? Hayır. Yeni öğe atasından büyük: ata aşağı inmeli.
yigin[3] = yigin[1]: ata (14) bir düzey aşağı kaydı. Takas yok; boşluk (turuncu) yukarı çıktı.
i /= 2 → i = 1: bir üstteki ataya geçilir.
i == 1: köke ulaşıldı, döngü biter.
yigin[1] = item: 15 yerine oturdu. Heap: 15, 12, 14, 10, 8, 6, 7.
void maksimum_yigin_ekle(eleman item) { if (YIGIN_DOLUMU(n)) { yigin_dolu(); /* hata iletisi */ return; } int i = ++n; while (i != 1) { /* i == 1: kökteyiz */ if (item.x <= yigin[i / 2].x) break; yigin[i] = yigin[i / 2]; /* atayı aşağı kaydır */ i /= 2; /* bir üst ataya bak */ } yigin[i] = item; }
Ekleme · kodu adım adım çalıştır
Kod izleme: maksimum_yigin_ekle(109)
ekle(109): tüm öğelerden büyük bir değer. En kötü durum: köke kadar çıkacak.
YIGIN_DOLUMU(7) yanlış: 7 ≠ 199, yer var.
i = ++n = 8: tam ağacın ilk boş konumu. Yeni öğenin yeri kesinlikle hazır; ama önce atasıyla kıyaslanacak, item henüz yazılmaz.
i = 8 ≠ 1: henüz kökte değiliz, döngüye girilir.
109 ≤ 10? Hayır. Yeni öğe atasından büyük: ata aşağı inmeli.
yigin[8] = yigin[4]: ata (10) bir düzey aşağı kaydı. Takas yok; boşluk (turuncu) yukarı çıktı.
i /= 2 → i = 4: bir üstteki ataya geçilir.
i = 4 ≠ 1: henüz kökte değiliz, döngüye girilir.
109 ≤ 12? Hayır. Yeni öğe atasından büyük: ata aşağı inmeli.
yigin[4] = yigin[2]: ata (12) bir düzey aşağı kaydı. Takas yok; boşluk (turuncu) yukarı çıktı.
i /= 2 → i = 2: bir üstteki ataya geçilir.
i = 2 ≠ 1: henüz kökte değiliz, döngüye girilir.
109 ≤ 15? Hayır. Yeni öğe atasından büyük: ata aşağı inmeli.
yigin[2] = yigin[1]: ata (15) bir düzey aşağı kaydı. Takas yok; boşluk (turuncu) yukarı çıktı.
i /= 2 → i = 1: bir üstteki ataya geçilir.
i == 1: köke ulaşıldı, döngü biter.
yigin[1] = item: 109 yerine oturdu. Heap: 109, 15, 14, 12, 8, 6, 7, 10.
void maksimum_yigin_ekle(eleman item) { if (YIGIN_DOLUMU(n)) { yigin_dolu(); /* hata iletisi */ return; } int i = ++n; while (i != 1) { /* i == 1: kökteyiz */ if (item.x <= yigin[i / 2].x) break; yigin[i] = yigin[i / 2]; /* atayı aşağı kaydır */ i /= 2; /* bir üst ataya bak */ } yigin[i] = item; }
Ekleme
Art arda ekleme: +107, +3, +15
Şu an: 109, 15, 14, 12, 8, 6, 7, 10. Sırada 107, 3 ve 15 var.
Önce yeni yer açılır: tam ağacın ilk boş konumu (9).
107 > 12: ata 12 aşağı kaydırılır, boşluk 4. konuma çıkar.
107 > 15: ata 15 aşağı kaydırılır, boşluk 2. konuma çıkar.
107 ≤ 109: ata yeterince büyük, durulur.
107, 2. konuma yazılır. Heap özelliği yeniden sağlandı.
Önce yeni yer açılır: tam ağacın ilk boş konumu (10).
3 ≤ 8: ata yeterince büyük, durulur.
3, 10. konuma yazılır. Heap özelliği yeniden sağlandı.
Önce yeni yer açılır: tam ağacın ilk boş konumu (11).
15 > 8: ata 8 aşağı kaydırılır, boşluk 5. konuma çıkar.
15 ≤ 107: ata yeterince büyük, durulur.
15, 5. konuma yazılır. Heap özelliği yeniden sağlandı.
3 eklenirken hiç kaydırma olmaz (en iyi durum, Ω(1)); 109 köke kadar çıkmıştı (en kötü durum, O(lg n)).
Ekleme
Ekleme maliyeti: en çok ağacın yüksekliği kadar
| Durum | Örnek | Kaydırma | Maliyet |
|---|---|---|---|
| En iyi | 3'ün eklenmesi | 0 | Ω(1) |
| En kötü | 109'un eklenmesi | yükseklik kadar | O(lg n) |
| Genel | — | 0 … ⌊lg n⌋ | O(lg n) |
Silme
Maksimum heap'ten öğe alma: kök alınır, sondaki yerleştirilir
Veri: 30, 15, 28, 11, 14, 3, 6, 10, 9. İlk alınacak öğe kökteki 30.
Kök (30) alındı. Sondaki öğe 9 çıkarıldı, kökteki boşluğa aday.
İki çocuktan büyüğü (28) deger'den (9) büyük → yukarı taşındı.
9, 3. konuma yerleşti.
Her adımda dikkat edilecek 3 değer var: iki çocuk ve sondaki öğe. En büyüğü i'nin gösterdiği yere taşınır; i yerleştirilecek konumu, j çocukları gösterir.
Silme
maksimum_yigin_al: düzeltilmiş sürüm
bool maksimum_yigin_al(eleman *cikan) { if (YIGIN_BOSMU(n)) return false; *cikan = yigin[1]; /* kök döndürülecek */ eleman deger = yigin[n--]; /* yeri aranacak */ int i = 1, j = 2; while (j <= n) { if (j < n && yigin[j].x < yigin[j + 1].x) j++; if (deger.x >= yigin[j].x) break; yigin[i] = yigin[j]; /* çocuğu yukarı al */ i = j; j *= 2; } yigin[i] = deger; return true; }
Boş heap'te return 0; yazmak derleme hatası verir (tamsayı bir struct'a dönüşmez).
Önerilen çözüm: bool dönüş (başarı/başarısızlık) ve alınan öğe işaretçi parametre ile döndürülür.
j < n neden?yigin[j+1] okumadan önce sağ çocuğun var olduğunu garanti eder.
Silme · kodu adım adım çalıştır
Kod izleme: maksimum_yigin_al (30)
Çağrı: heap'te 9 öğe var; kök 30. Çıkan öğe e'ye yazılacak.
YIGIN_BOSMU(9) yanlış; alınacak öğe var.
*cikan = yigin[1] = 30: tanım gereği ilk alınacak öğe dizinin ilk öğesidir.
deger = yigin[n--] = 9, n = 8. Tam ağaç bozulmasın diye sondaki öğe çıkarıldı; ona kökten başlayarak yer aranacak.
i = 1 (deger'in aday konumu), j = 2 (i'nin sol çocuğu).
j = 2 ≤ n = 8: i'nin çocuğu var, döngüye girilir.
Sağ çocuk daha büyük (15 < 28) → j++, j = 3.
9 ≥ 28? Hayır: büyük çocuk yukarı çıkmalı.
yigin[1] = yigin[3]: 28 bir düzey yukarı taşındı; boşluk 3. konuma indi.
i = 3, j = 6: taşınan değerin eski konumuna ilerlenir.
j = 6 ≤ n = 8: i'nin çocuğu var, döngüye girilir.
Sağ çocuk daha büyük (3 < 6) → j++, j = 7.
9 ≥ 6? Evet: deger iki çocuktan da büyük/eşit → break.
yigin[3] = deger (9). Heap: 28, 15, 9, 11, 14, 3, 6, 10.
return true; çağıran cikan içinde 30 değerini alır. Yol boyu ≤ ⌊lg n⌋ düzey → O(lg n).
bool maksimum_yigin_al(eleman *cikan) { if (YIGIN_BOSMU(n)) return false; *cikan = yigin[1]; /* kök döndürülecek */ eleman deger = yigin[n--]; /* yeri aranacak */ int i = 1, j = 2; while (j <= n) { if (j < n && yigin[j].x < yigin[j + 1].x) j++; if (deger.x >= yigin[j].x) break; yigin[i] = yigin[j]; /* çocuğu yukarı al */ i = j; j *= 2; } yigin[i] = deger; return true; }
Silme · kodu adım adım çalıştır
Kod izleme: ikinci maksimum_yigin_al (28)
İkinci çağrı: 28, 15, 9, 11, 14, 3, 6, 10. Bu kez sondaki öğe (10) yaprağa kadar inecek.
YIGIN_BOSMU(8) yanlış; alınacak öğe var.
*cikan = yigin[1] = 28: tanım gereği ilk alınacak öğe dizinin ilk öğesidir.
deger = yigin[n--] = 10, n = 7. Tam ağaç bozulmasın diye sondaki öğe çıkarıldı; ona kökten başlayarak yer aranacak.
i = 1 (deger'in aday konumu), j = 2 (i'nin sol çocuğu).
j = 2 ≤ n = 7: i'nin çocuğu var, döngüye girilir.
Sol çocuk (15) ≥ sağ çocuk (9): j = 2 kalır.
10 ≥ 15? Hayır: büyük çocuk yukarı çıkmalı.
yigin[1] = yigin[2]: 15 bir düzey yukarı taşındı; boşluk 2. konuma indi.
i = 2, j = 4: taşınan değerin eski konumuna ilerlenir.
j = 4 ≤ n = 7: i'nin çocuğu var, döngüye girilir.
Sağ çocuk daha büyük (11 < 14) → j++, j = 5.
10 ≥ 14? Hayır: büyük çocuk yukarı çıkmalı.
yigin[2] = yigin[5]: 14 bir düzey yukarı taşındı; boşluk 5. konuma indi.
i = 5, j = 10: taşınan değerin eski konumuna ilerlenir.
j = 10 > n = 7: i bir yaprak; döngüden çıkılır.
yigin[5] = deger (10). Heap: 15, 14, 9, 11, 10, 3, 6.
return true; çağıran cikan içinde 28 değerini alır. Yol boyu ≤ ⌊lg n⌋ düzey → O(lg n).
bool maksimum_yigin_al(eleman *cikan) { if (YIGIN_BOSMU(n)) return false; *cikan = yigin[1]; /* kök döndürülecek */ eleman deger = yigin[n--]; /* yeri aranacak */ int i = 1, j = 2; while (j <= n) { if (j < n && yigin[j].x < yigin[j + 1].x) j++; if (deger.x >= yigin[j].x) break; yigin[i] = yigin[j]; /* çocuğu yukarı al */ i = j; j *= 2; } yigin[i] = deger; return true; }
Silme
Heap boşalana dek alınır: çıkış sırası azalan sıradır
Şu an: 15, 14, 9, 11, 10, 3, 6. Çıkanlar: 30, 28.
15 alındı; sondaki öğe 6 4. konuma yerleşti. Çıkış sırası: 30, 28, 15
14 alındı; sondaki öğe 3 5. konuma yerleşti. Çıkış sırası: 30, 28, 15, 14
11 alındı; sondaki öğe 3 4. konuma yerleşti. Çıkış sırası: 30, 28, 15, 14, 11
10 alındı; sondaki öğe 3 3. konuma yerleşti. Çıkış sırası: 30, 28, 15, 14, 11, 10
9 alındı; sondaki öğe 3 2. konuma yerleşti. Çıkış sırası: 30, 28, 15, 14, 11, 10, 9
6 alındı; sondaki öğe 3 1. konuma yerleşti. Çıkış sırası: 30, 28, 15, 14, 11, 10, 9, 6
3 alındı; sondaki öğe 3 son öğeydi, heap boşaldı. Çıkış sırası: 30, 28, 15, 14, 11, 10, 9, 6, 3
Silme
Silme maliyeti ve sondaki öğenin yolculuğu
1. silmede 9, üç düzey inmeden 3. konuma yerleşti.
2. silmede 10 yaprağa (5. konum) kadar indi: en kötü durum.
Döngü her turda bir düzey iner: en çok ⌊lg n⌋ tur, tur başına 2 karşılaştırma → O(lg n).
n kez silmek öğeleri azalan sırada verir: bu, heap sort'un fikridir.
Silme
Yordamları daha güzel yazmak: global yerine yapı
typedef struct { eleman *a; int n, kapasite; } Yigin; Yigin *yigin_olustur(int kapasite) { Yigin *h = malloc(sizeof *h); if (h == NULL) return NULL; h->a = malloc((kapasite + 1) * sizeof *h->a); if (h->a == NULL) { free(h); return NULL; } h->n = 0; h->kapasite = kapasite; return h; } void yigin_yok_et(Yigin *h) { if (h) { free(h->a); free(h); } }
Başarı TRUE/FALSE ile bildirilsin; öğe ve n referansla aktarılsın.
Dizi ve n bir yapıda: birden çok heap olabilir; malloc hep denetlenir.
Silme
Min hâlleri: ad ve karşılaştırmalar çevrilir
void minimum_yigin_ekle(eleman item) { /* ... dolu denetimi aynı ... */ int i = ++n; while (i != 1) { if (item.x >= yigin[i / 2].x) break; /* >= ! */ yigin[i] = yigin[i / 2]; i /= 2; } yigin[i] = item; }
| Yer | Maks. | Min. |
|---|---|---|
| ekle: dur | item <= ata | item >= ata |
| al: çocuk | büyüğü | küçüğü |
| al: dur | deger >= [j] | deger <= [j] |
Silme
Öncelik zamanla değişirse: uçakların yakıtı
Her uçağın saniyede harcadığı yakıt farklıdır: her saniye kesme (interrupt) ile yakıtlar günlenmeli.
Önceliği artan öğe yukarı, azalan öğe aşağı kaydırılır: O(lg n). Konum k bir indeks tablosunda tutulur.
/* k konumundaki öğenin önceliği arttı: yukarı kaydır */ void oncelik_artir(int k, int yeni) { eleman e = yigin[k]; e.x = yeni; while (k != 1 && e.x > yigin[k / 2].x) { yigin[k] = yigin[k / 2]; k /= 2; } yigin[k] = e; }
Karşılaştırma
Niçin heap? Basit dizi ya da liste olmaz mı?
| # | Yapı | Ekle | Sil (en büyük) | Neden |
|---|---|---|---|---|
| 1 | Sırasız dizi | Θ(1) | Θ(n) | Sona eklenir; en büyüğü bulmak için tamamına bakılır. |
| 2 | Sırasız liste | Θ(1) | Θ(n) | Başa eklenir; yine hepsinde gezinilir. |
| 3 | Sıralı dizi (küçükten büyüğe) | O(n) | Θ(1) | binsearch O(lg n) + n kaydırma; en büyük sonda. |
| 4 | Sıralı bağlı liste (büyükten küçüğe) | O(n) | Θ(1) | Yeri bulunur; baştan silinir, baş günlenir. |
| 5 | Maksimum heap | O(lg n) | O(lg n) | Yalnızca bir kök–yaprak yolu. |
En iyi (Ω) ve en kötü (O) durum aynı derecedense sınır sıkıdır ve Θ ile ifade edilir. Sıralı dizide binsearch kullanılmazsa ekleme O(n) değil Θ(n) olur.
Karşılaştırma
Maliyet tablosunu sayılara dökelim
| # | Ekle | Sil |
|---|---|---|
| 1 · sırasız dizi | 1 | 2.000.000 |
| 2 · sırasız liste | 1 | 2.000.000 |
| 3 · sıralı dizi | 2.000.000 | 1 |
| 4 · sıralı liste | 2.000.000 | 1 |
| 5 · maks. heap | ≈ 21 | ≈ 21 |
Bir komut = 1 saniye varsayımı; lg 2.000.000 ≈ 20,9 (yuvarlanarak 21).
Karşılaştırma
“Kim anlayacak benim algoritmanın kötü olduğunu?”
Karşılaştırma
İleri düzey: binomial ve Fibonacci heap'leri
| İşlem | İkili heap | Binomial heap | Fibonacci heap* |
|---|---|---|---|
| ekle | O(lg n) | O(lg n) | O(1) |
| en küçüğü bul | O(1) | O(lg n) | O(1) |
| en küçüğü sil | O(lg n) | O(lg n) | O(lg n) |
| anahtar azalt | O(lg n) | O(lg n) | O(1) |
| iki heap'i birleştir | O(n) | O(lg n) | O(1) |
* Fibonacci heap süreleri amortize (ortalama, işlem dizisi üzerinden) sürelerdir (CLRS). Dijkstra ve Prim gibi çok sayıda “anahtar azalt” yapan algoritmalarda kuramsal kazanç sağlar.
İleri Konular
Heapify: diziyi yerinde heap'e çevirmek
void yigin_kur(eleman a[], int n) { for (int k = n / 2; k >= 1; k--) { eleman deger = a[k]; int i = k, j = 2 * k; while (j <= n) { if (j < n && a[j].x < a[j + 1].x) j++; if (deger.x >= a[j].x) break; a[i] = a[j]; i = j; j *= 2; } a[i] = deger; } }
Yapraklar (k > n/2) zaten tek öğelik heap'tir. k = n/2'den köke doğru her düğüm aşağı kaydırılır: iki çocuğu heap olan bir kök düzeltilir.
n kez ekle O(n lg n) sürer; aşağıdan yukarı kurma O(n).
maksimum_yigin_al içindeki döngünün aynısı (Horowitz'de adjust).
Heapify · kodu adım adım çalıştır
Kod izleme: yigin_kur — k = 3 ve k = 2
Girdi: 5, 3, 8, 9, 1, 12, 7 (henüz heap değil). n/2 = 3'ten başlanır.
k = 3: 3 köklü alt ağaç düzeltilir. Yeşil düğümler zaten birer heap'in köküdür (yapraklar ve daha önce düzeltilenler).
deger = a[3] = 8; i = 3, j = 6. Silmedeki aşağı kaydırmanın aynısı başlıyor.
Çocuklar 12 ve 7: büyüğü a[6] = 12.
8 < 12 → a[3] = a[6]; boşluk 6. konuma iner.
i = 6, j = 12: boşluğun yeni yerinden devam.
j = 12 > n: i = 6 yaprak, döngü biter.
a[6] = deger (8). 3 köklü alt ağaç artık bir heap: 5, 3, 12, 9, 1, 8, 7.
k = 2: 2 köklü alt ağaç düzeltilir. Yeşil düğümler zaten birer heap'in köküdür (yapraklar ve daha önce düzeltilenler).
deger = a[2] = 3; i = 2, j = 4. Silmedeki aşağı kaydırmanın aynısı başlıyor.
Çocuklar 9 ve 1: büyüğü a[4] = 9.
3 < 9 → a[2] = a[4]; boşluk 4. konuma iner.
i = 4, j = 8: boşluğun yeni yerinden devam.
j = 8 > n: i = 4 yaprak, döngü biter.
a[4] = deger (3). 2 köklü alt ağaç artık bir heap: 5, 9, 12, 3, 1, 8, 7.
void yigin_kur(eleman a[], int n) { for (int k = n / 2; k >= 1; k--) { eleman deger = a[k]; int i = k, j = 2 * k; while (j <= n) { if (j < n && a[j].x < a[j + 1].x) j++; if (deger.x >= a[j].x) break; a[i] = a[j]; i = j; j *= 2; } a[i] = deger; } }
Heapify · kodu adım adım çalıştır
Kod izleme: yigin_kur — k = 1
k = 1: 1 köklü alt ağaç düzeltilir. Yeşil düğümler zaten birer heap'in köküdür (yapraklar ve daha önce düzeltilenler).
deger = a[1] = 5; i = 1, j = 2. Silmedeki aşağı kaydırmanın aynısı başlıyor.
Çocuklar 9 ve 12: büyüğü a[3] = 12.
5 < 12 → a[1] = a[3]; boşluk 3. konuma iner.
i = 3, j = 6: boşluğun yeni yerinden devam.
Çocuklar 8 ve 7: büyüğü a[6] = 8.
5 < 8 → a[3] = a[6]; boşluk 6. konuma iner.
i = 6, j = 12: boşluğun yeni yerinden devam.
j = 12 > n: i = 6 yaprak, döngü biter.
a[6] = deger (5). 1 köklü alt ağaç artık bir heap: 12, 9, 8, 3, 1, 5, 7.
k = 0: döngü bitti. Tüm dizi bir maksimum heap: 12, 9, 8, 3, 1, 5, 7.
void yigin_kur(eleman a[], int n) { for (int k = n / 2; k >= 1; k--) { eleman deger = a[k]; int i = k, j = 2 * k; while (j <= n) { if (j < n && a[j].x < a[j + 1].x) j++; if (deger.x >= a[j].x) break; a[i] = a[j]; i = j; j *= 2; } a[i] = deger; } }
İleri Konular
Heapify neden O(n)?
Düğümlerin yarısı yapraktır ve hiç inmez; çeyreği en çok 1, sekizde biri en çok 2 düzey iner… Toplam iş:
Σh≥0 ⌈n / 2h+1⌉ · O(h) = O(n · Σ h/2h) = O(2n) = O(n)
İleri Konular
d-ary heap: her düğümün d çocuğu
| İkili (d = 2) | d-ary (0 tabanlı) | |
|---|---|---|
| ata | (i−1)/2 | (i−1)/d |
| çocuklar | 2i+1, 2i+2 | d·i+1 … d·i+d |
| yükseklik | ≈ lg n | ≈ logd n |
| ekle / anahtar artır | O(lg n) | O(logd n) — daha hızlı |
| kökü sil | O(lg n) | O(d · logd n) — d çocuk kıyaslanır |
Ekleme/anahtar azaltma çok, silme az ise (ör. yoğun çizgelerde Dijkstra) d > 2 seçmek kazançlıdır; sığ ağaç önbellek (cache) dostudur.
İleri Konular
Öncelikli kuyruk nerelerde kullanılır?
İşlem (process) ve kesme zamanlaması, zamanlayıcı olayları.
Dijkstra en kısa yol ve Prim MST: en yakın düğümü seçmek.
Heap kurulur, n kez kök alınır: O(n lg n), yerinde sıralama.
Olaylar zaman damgasına göre minimum heap'ten çekilir.
Boyutu k olan min heap: akan veride O(n lg k).
En küçük iki frekansı art arda birleştirir.
Özet
Karmaşıklık özeti (n öğeli ikili heap)
| İşlem | Zaman | Ek bellek | Not |
|---|---|---|---|
| en büyüğe bak | O(1) | O(1) | yigin[1] |
| ekle | Ω(1) … O(lg n) | O(1) | yukarı kaydırma |
| en büyüğü al | O(lg n) | O(1) | aşağı kaydırma |
| önceliği değiştir | O(lg n) | O(1) | konum biliniyorsa |
| aşağıdan yukarı kurma | O(n) | O(1) | yerinde |
| n kez ekleyerek kurma | O(n lg n) | O(1) | |
| keyfi öğeyi ara | O(n) | O(1) | heap arama yapısı değildir |
Özet
Sık yapılan hatalar
i/2, 2i) ve 0 tabanlı ((i−1)/2, 2i+1) formülleri karıştırmak.yigin[199] dışarı yazılır.eleman döndüren yordamda return 0; yazmak (derleme hatası).j < n sınamasını unutup yigin[j+1]'i okumak (dizi dışı).Özet
Sınav tarzı soru: ekle, ekle, … al, al
yigin[1..6] = 14, 12, 7, 10, 8, 6 olan maksimum heap'e sırasıyla 15, 109, 107, 3, 15 ekleniyor; ardından maksimum_yigin_al iki kez çağrılıyor.(i) Eklemelerden sonra dizinin içeriği nedir? (ii) Alınan öğeler ve son dizi nedir? (iii) Hangi ekleme en az, hangisi en çok kaydırma yaptı?
Çözüm
109, 107, 14, 15, 15, 6, 7, 10, 12, 3, 8(n = 11).- Alınanlar 109 ve 107; son dizi
15, 15, 14, 12, 3, 6, 7, 10, 8(n = 9). - 3'ün eklenmesi hiç kaydırma yapmadı (Ω(1)); 109 köke kadar 3 kaydırma yaptı (O(lg n)).
Özet
Alıştırmalar
- Boş bir maksimum heap'e 20, 5, 30, 10, 40 ekleyin; her eklemeden sonra diziyi yazın.
maksimum_yigin_alyordamını minimum heap için yazın. Hangi satırlar değişir?- n = 1000 öğeli bir heap'te ekleme en çok kaç kaydırma yapar?
- 1 tabanlı bir heap'te 13. düğümün atası ve çocukları hangi indislerdedir?
5, 3, 8, 9, 1, 12, 7dizisine yigin_kur yerine art ardaekleuygulanırsa sonuç aynı mı olur?- Bir heap'te en küçük öğe nerelerde olabilir? Bulma maliyeti nedir?
Cevapları göster
20→20, 5→30, 5, 20→30, 10, 20, 5→40, 30, 20, 5, 10.- Çocuk seçiminde
[j] > [j+1], durma koşulundadeger.x <= yigin[j].x; adıminimum_yigin_al. - Yükseklik ⌈lg 1001⌉ = 10 düzey → en çok 9 kaydırma (⌊lg 1000⌋ = 9).
- Ata 13/2 = 6; çocuklar 26 ve 27 (n ≥ 27 ise).
- Hayır; art arda ekleme
12, 8, 9, 3, 1, 5, 7verir. İkisi de geçerli heap'tir ama farklıdır. - Yalnızca yapraklarda (indis > n/2); yaklaşık n/2 yaprak taranır → O(n).
Özet
Bölüm özeti
İlk alınacak öğe en yüksek (max) ya da en düşük (min) değerli öğedir.
Tam ikili ağaç + ata ≥ çocuk. 1 tabanlı dizide ata i/2, çocuklar 2i ve 2i+1.
İlk boş konum açılır, büyük atalar aşağı kaydırılır: O(lg n).
Kök alınır, sondaki öğeye kökten yer aranır, büyük çocuklar yukarı alınır: O(lg n).
Diğer yapılar bir işlemi Θ(n)'e iter; heap ikisini de logaritmik tutar.
Heapify O(n), d-ary heap, binomial ve Fibonacci heap'leri.
Özet
Kaynaklar ve devam
- E. Horowitz, S. Sahni, S. Anderson-Freed, Fundamentals of Data Structures in C, Bölüm 5.6 (Heaps), Bölüm 7 (Heap Sort).
- T. H. Cormen vd., Introduction to Algorithms (CLRS), Bölüm 6 (Heapsort, öncelikli kuyruklar) ve Fibonacci heap'leri bölümü.
- R. Sedgewick, Algorithms in C, Kısım 1–4, Bölüm 9 (Priority Queues and Heapsort).