SEN2212 · 7. hafta · Using heap structure and implementing them using Java.

İkili heap: dizi gösterimi, percolate, buildHeap, heapsort

Binary heap'in dizi içinde tutulması, insert (percolate up) ve deleteMin (percolate down) izleri, O(n) buildHeap ve heapsort. Sınavın klasik sorusu: şu işlemlerden sonra dizi neye benzer?

binary heapmin-heapmax-heapheap-order propertycomplete binary treepercolate uppercolate downbuildHeapheapsortdeleteMin

Heap, öncelik kuyruğunun (priority queue) arkasındaki yapıdır: en küçük (ya da en büyük) elemana her an O(1)‘de ulaşırsın, ekleme ve silme O(log n). Ağaç gibi düşünülür ama dizi içinde tutulur, işaretçi yoktur. Sınavda neredeyse hep aynı soru gelir: “Şu sırayla ekle / şu kadar deleteMin yap / buildHeap uygula, son diziyi yaz.” Bu sayfadaki iki kaydırma işlemini (percolate up ve percolate down) doğru yapan biri bu soruları kaçırmaz.

Altın kurallar:

  1. Yapı kuralı: heap tam (complete) ikili ağaçtır. Seviyeler soldan sağa doldurulur, boşluk yok.
  2. Sıra kuralı (min-heap): her düğüm çocuklarından küçük ya da eşittir. En küçük eleman kökte.
  3. Ekleme yukarı, silme aşağı. insert sona koyar ve yukarı kaydırır. deleteMin son elemanı köke koyar ve aşağı kaydırır.
  4. Aşağı inerken küçük çocukla yer değiştir. Büyük çocukla değiştirirsen sıra kuralı bozulur.

Dizi gösterimi

Ağaç tam olduğu için düğümleri seviye sırasıyla diziye yazmak yeterli. Bu derste (ve Weiss’te) öncelik kuyruğu heap’i 1. indisten başlar, a[0] boş kalır. O zaman formüller çok temiz olur:

İstenenFormül (1-indisli)0-indisli karşılığı
ebeveyni / 2(i - 1) / 2
sol çocuk2i2i + 1
sağ çocuk2i + 12i + 2
index:  1   2   3   4   5   6   7   8
array: [6, 14,  9, 20, 17, 11, 30, 25]

              6
           /     \
         14       9
        /  \     /  \
      20    17  11   30
     /
   25

25’in indisi 8, ebeveyni 8 / 2 = 4, yani 20. Her ebeveyn çocuklarından küçük: bu bir min-heap. Ama kardeşler arasında (14 ile 9 gibi) bir sıra yok. Heap, BST değildir.

n elemanlı heap’in yüksekliği ⌊log⁡2n⌋\lfloor \log_2 n \rfloor. 8 eleman için 3.

insert: percolate up

  1. Yeni elemanı ilk boş yere (dizinin sonuna, index size + 1) koy.
  2. Ebeveyninden küçük olduğu sürece ebeveyniyle yer değiştir.

Yukarıdaki heap’e 8 ekleyelim:

8 index 9'a: ebeveyn a[4]=20 > 8  → yukari
8 index 4'te: ebeveyn a[2]=14 > 8 → yukari
8 index 2'de: ebeveyn a[1]=6 < 8  → dur

[6, 8, 9, 14, 17, 11, 30, 25, 20]
              6
           /     \
          8       9
        /  \     /  \
      14    17  11   30
     /  \
   25    20

En kötü durumda yeni eleman köke kadar çıkar: O(log n).

deleteMin: percolate down

  1. Kökteki elemanı (minimum) sakla, döndüreceğiz.
  2. Son elemanı köke taşı, boyutu 1 azalt.
  3. Kökten başlayarak: iki çocuktan küçük olanı seç. Çocuk taşıdığın değerden küçükse yer değiştir, aşağı in. Değilse dur.

Az önceki 9 elemanlı heap’te deleteMin:

6 cikar, son eleman 20 koke:
[20, 8, 9, 14, 17, 11, 30, 25]
i=1: cocuklar 8, 9   → 8 < 20, degistir
i=2: cocuklar 14, 17 → 14 < 20, degistir
i=4: cocuk 25 (index 8) > 20 → dur

[8, 14, 9, 20, 17, 11, 30, 25]

TUZAK · Silinen elemanın yerine son eleman gelir. Köke “küçük çocuğu çıkarmak” diye bir şey yapılmaz. Önce son eleman köke konur, sonra aşağı kaydırılır. Böylece ağaç tam kalır, boşluk oluşmaz.

buildHeap: O(n) kurulum

Elinde sırasız n eleman varsa iki yol var: n kez insert (O(n log n)) ya da Floyd yöntemi. Floyd: elemanları olduğu gibi diziye koy, sonra son iç düğümden (n/2) köke doğru her düğümde percolateDown çağır. Yapraklar (n/2’den sonrakiler) zaten tek başına heap’tir, onlara dokunulmaz.

[15, 12, 10, 8, 4, 6, 2, 9, 1, 3] için (n = 10, i = 5, 4, 3, 2, 1):

    percDown(5) -> [15, 12, 10, 8, 3, 6, 2, 9, 1, 4]
    percDown(4) -> [15, 12, 10, 1, 3, 6, 2, 9, 8, 4]
    percDown(3) -> [15, 12, 2, 1, 3, 6, 10, 9, 8, 4]
    percDown(2) -> [15, 1, 2, 8, 3, 6, 10, 9, 12, 4]
    percDown(1) -> [1, 3, 2, 8, 4, 6, 10, 9, 12, 15]
  result: [1, 3, 2, 8, 4, 6, 10, 9, 12, 15]  moves=8

Son adımda 15 kökten en alta (index 10) kadar iner: 1, 3 ve 4 birer seviye yukarı çıkar.

Neden O(n)? percolateDown bir düğümde en fazla o düğümün yüksekliği kadar iş yapar. Düğümlerin yarısı yaprak (iş 0), çeyreği yükseklik 1, sekizde biri yükseklik 2, … Toplam:

∑hn2h+1⋅h  ≤  n∑h≥1h2h+1=n\sum_{h} \frac{n}{2^{h+1}} \cdot h \;\le\; n \sum_{h \ge 1} \frac{h}{2^{h+1}} = n

Yani iş düğümlerin çoğunun bulunduğu altta az, düğümlerin az olduğu üstte çok. Ortalama sabit, toplam O(n). insert ile kurarken tersi olur: elemanların çoğu altta ve her biri köke kadar çıkabilir.

Java’da MinHeap

public class MinHeap {
    private int[] array = new int[11];   // array[0] kullanilmaz
    private int size = 0;

    public void insert(int x) {
        if (size + 1 == array.length)
            array = java.util.Arrays.copyOf(array, array.length * 2);
        int hole = ++size;
        while (hole > 1 && x < array[hole / 2]) {
            array[hole] = array[hole / 2];   // ebeveyni asagi al
            hole /= 2;
        }
        array[hole] = x;
    }

    public int findMin() { return array[1]; }

    public int deleteMin() {
        if (size == 0) throw new RuntimeException("empty heap");
        int min = array[1];
        array[1] = array[size--];            // son eleman koke
        percolateDown(1);
        return min;
    }

    private void percolateDown(int hole) {
        int tmp = array[hole];
        while (hole * 2 <= size) {
            int child = hole * 2;
            if (child != size && array[child + 1] < array[child]) child++;
            if (array[child] < tmp) array[hole] = array[child];
            else break;
            hole = child;
        }
        array[hole] = tmp;
    }

    public void buildHeap(int[] items) {
        array = new int[items.length + 1];
        size = items.length;
        for (int i = 0; i < items.length; i++) array[i + 1] = items[i];
        for (int i = size / 2; i > 0; i--) percolateDown(i);
    }

    public String toString() {           // array[1..size]
        return java.util.Arrays.toString(
            java.util.Arrays.copyOfRange(array, 1, size + 1));
    }
}

Kod değerleri her adımda takas etmez. Bir “delik” (hole) gezdirir ve elemanı en sonda bir kez yerine yazar. Takas 3 atama, delik kaydırma 1 atama yapar.

MinHeap h = new MinHeap();
for (int x : new int[]{6, 14, 9, 20, 17, 11, 30, 25}) h.insert(x);
h.insert(8);
System.out.println("+8 -> " + h);
System.out.println("deleteMin = " + h.deleteMin() + " -> " + h);
+8 -> [6, 8, 9, 14, 17, 11, 30, 25, 20]
deleteMin = 6 -> [8, 14, 9, 20, 17, 11, 30, 25]

Heapsort

Heap ile sıralama: diziyi max-heap yap, sonra en büyüğü (kök) sona taşı ve heap’i bir küçült. Ek dizi gerekmez. Heapsort’ta dizi 0. indisten başlar (sıralanacak dizi zaten böyle), çocuklar 2i + 1 ve 2i + 2.

  1. buildMaxHeap: i = n/2 - 1’den 0’a, her birinde (max için) percolateDown.
  2. n - 1 kez: a[0] ile heap’in son elemanını takasla, heap boyunu 1 azalt, a[0]‘ı aşağı kaydır.

[4, 10, 3, 5, 1] için:

== heapsort [4, 10, 3, 5, 1]
  after buildMaxHeap: [10, 5, 3, 4, 1]
  after deleteMax #1: [5, 4, 3, 1, 10]  (heap part size 4)
  after deleteMax #2: [4, 1, 3, 5, 10]  (heap part size 3)
  after deleteMax #3: [3, 1, 4, 5, 10]  (heap part size 2)
  after deleteMax #4: [1, 3, 4, 5, 10]  (heap part size 1)

Her adımda dizinin sağında sıralı kısım bir büyür. buildMaxHeap: i=1 (10) çocukları 5, 1’den büyük, değişmez; i=0 (4) çocukları 10, 3, büyüğü 10 yukarı, 4 index 1’de çocukları 5, 1, büyüğü 5 yukarı. Sonuç [10, 5, 3, 4, 1].

ÖzellikHeapsort
ZamanO(n log n) en iyi, ortalama, en kötü
Ek bellekO(1), yerinde (in-place)
Kararlı mı?Hayır (eşit anahtarların sırası değişebilir)

Sık yapılan hatalar

Yanlış düşünceDoğrusu
Heap BST gibi sıralıdır, sol < sağSadece ebeveyn-çocuk sırası var; kardeşler serbest
deleteMin’de küçük çocuk köke çıkar, boşluk aşağıda kalırSon eleman köke konur, aşağı kaydırılır; ağaç tam kalır
Aşağı kaydırırken ilk (sol) çocukla değiştiririmİki çocuğun küçüğüyle değiştir (max-heap’te büyüğüyle)
buildHeap = n kez insertFloyd: n/2’den 1’e percolateDown; O(n) ve farklı dizi verebilir
buildHeap kökten başlarSon iç düğümden (n/2) köke doğru gider
1-indisli dizide çocuklar 2i+1, 2i+2O 0-indisli formül; 1-indisli: 2i, 2i+1
Heap’te arama O(log n)Herhangi bir elemanı aramak O(n); sadece min O(1)
Heapsort kararlıdırDeğildir; kararlı O(n log n) istiyorsan merge sort

Kendini test et

Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.

Soru 1kolay

In a binary heap stored in an array starting at index 1, the node at index 13 has which parent index and which child indices?

Cevabı göster
parent 6, children 26 and 27

1-indisli dizide: ebeveyn ⌊i/2⌋\lfloor i/2 \rfloor, sol çocuk 2i2i, sağ çocuk 2i+12i+1.
13 / 2 = 6 (tam sayı bölmesi), 2·13 = 26, 2·13 + 1 = 27. Bu çocuklar ancak dizi boyutu en az 26 ise vardır.

Soru 2kolay

Which array (index 0 unused, listed from index 1) is a valid min-heap?

  1. [2, 5, 3, 8, 6, 4, 9]
  2. [2, 5, 3, 4, 6, 7, 9]
  3. [2, 5, 3, 8, 6, 1, 9]
Cevabı göster
[2, 5, 3, 8, 6, 4, 9]

Her i >= 2 için a[i] >= a[i/2] olmalı.
İkincisinde a[4] = 4, ebeveyni a[2] = 5'ten küçük: bozuk.
Üçüncüsünde a[6] = 1, ebeveyni a[3] = 3'ten küçük: bozuk.
Birincisinde 5, 3 >= 2; 8, 6 >= 5; 4, 9 >= 3. Geçerli.

Soru 3kolay

What is the height of a binary heap containing 100 elements? (A single node has height 0.)

Cevabı göster
6

Heap tam (complete) ikili ağaçtır, yüksekliği ⌊log⁡2n⌋\lfloor \log_2 n \rfloor. 26=64≤100<128=272^6 = 64 \le 100 < 128 = 2^7, yani 6.

Soru 4kolay

Give the time complexity of each min-heap operation: findMin, insert, deleteMin, buildHeap (n items).

Cevabı göster
O(1), O(log n), O(log n), O(n)

En küçük eleman hep a[1]'de: O(1). insert ve deleteMin en fazla yükseklik kadar (log n) adım kaydırır.
buildHeap, n kez insert yapsaydı O(n log n) olurdu; Floyd yöntemiyle (alttan percolate down) toplam iş düğümlerin yükseklikleri toplamıdır ve bu O(n).

Soru 5orta

Insert 10, 4, 7, 1, 8, 3 one at a time into an empty min-heap. Show the array (from index 1) after all insertions.

Cevabı göster
[1, 4, 3, 10, 8, 7]

+10: [10]. +4: index 2, ebeveyn 10 büyük, yukarı: [4, 10].
+7: index 3, ebeveyn 4 küçük, kalır: [4, 10, 7].
+1: index 4, ebeveyn 10 → yukarı (index 2), ebeveyn 4 → yukarı (index 1): [1, 4, 7, 10].
+8: index 5, ebeveyn 4 küçük: [1, 4, 7, 10, 8].
+3: index 6, ebeveyn 7 → yukarı (index 3), ebeveyn 1 küçük, dur: [1, 4, 3, 10, 8, 7].

Soru 6orta

Starting from the heap [1, 4, 3, 10, 8, 7], perform deleteMin twice. Show the array after both operations.

Cevabı göster
[4, 8, 7, 10]

1. deleteMin: 1 çıkar, son eleman 7 köke gelir: [7, 4, 3, 10, 8]. Çocuklar 4 ve 3, küçüğü 3 < 7: yer değiştir. 7 index 3'te, çocuğu yok: [3, 4, 7, 10, 8].
2. deleteMin: 3 çıkar, son eleman 8 köke: [8, 4, 7, 10]. Çocuklar 4, 7, küçüğü 4 < 8: [4, 8, 7, 10]. 8 index 2'de, çocuğu 10 (index 4) büyük, dur.

Soru 7orta

Apply buildHeap (Floyd's bottom-up method) to the array [9, 6, 8, 2, 5, 7, 1, 3] to make a min-heap. Show the resulting array.

Cevabı göster
[1, 2, 7, 3, 5, 9, 8, 6]

n = 8, percolateDown sırasıyla i = 4, 3, 2, 1.
i=4 (2): çocuğu 3, 2 küçük, değişmez.
i=3 (8): çocuklar 7, 1, küçüğü 1: [9, 6, 1, 2, 5, 7, 8, 3].
i=2 (6): çocuklar 2, 5 → 2 yukarı; 6 index 4'te, çocuğu 3 < 6 → 3 yukarı: [9, 2, 1, 3, 5, 7, 8, 6].
i=1 (9): çocuklar 2, 1 → 1 yukarı; 9 index 3'te, çocuklar 7, 8 → 7 yukarı: [1, 2, 7, 3, 5, 9, 8, 6].

Soru 8orta

Insert the same keys 9, 6, 8, 2, 5, 7, 1, 3 one at a time into an empty min-heap. Show the final array. Is it the same as the buildHeap result [1, 2, 7, 3, 5, 9, 8, 6]?

Cevabı göster
[1, 3, 2, 5, 6, 8, 7, 9], no

Tek tek eklemede her yeni eleman yukarı kaydırılır. İz: [9] → [6, 9] → [6, 9, 8] → [2, 6, 8, 9] → [2, 5, 8, 9, 6] → [2, 5, 7, 9, 6, 8] → [1, 5, 2, 9, 6, 8, 7] → +3: index 8, ebeveyn 9 → yukarı, ebeveyn 5 → yukarı, ebeveyn 1 dur: [1, 3, 2, 5, 6, 8, 7, 9].
İki yöntem de geçerli bir heap verir ama diziler farklı. Sınavda hangi yöntemin istendiğine dikkat et.

Soru 9orta

Insert 20, 15, 30, 10, 40 one at a time into an empty max-heap (index 1 based). Show the final array.

Cevabı göster
[40, 30, 20, 10, 15]

Max-heap'te ebeveyn çocuktan büyük ya da eşit olmalı; yeni eleman ebeveyninden büyükse yukarı çıkar.
[20] → [20, 15] → +30: ebeveyn 20 küçük, yukarı: [30, 15, 20] → +10: [30, 15, 20, 10] → +40: index 5, ebeveyn 15 → yukarı, ebeveyn 30 → yukarı: [40, 30, 20, 10, 15].

Soru 10zor

Apply buildHeap to [15, 12, 10, 8, 4, 6, 2, 9, 1, 3] to make a min-heap. Show the resulting array.

Cevabı göster
[1, 3, 2, 8, 4, 6, 10, 9, 12, 15]

n = 10, i = 5, 4, 3, 2, 1:
i=5 (4): çocuğu 3 → [15, 12, 10, 8, 3, 6, 2, 9, 1, 4].
i=4 (8): çocuklar 9, 1 → 1 yukarı: [15, 12, 10, 1, 3, 6, 2, 9, 8, 4].
i=3 (10): çocuklar 6, 2 → 2 yukarı: [15, 12, 2, 1, 3, 6, 10, 9, 8, 4].
i=2 (12): çocuklar 1, 3 → 1 yukarı; index 4'te çocuklar 9, 8 → 8 yukarı: [15, 1, 2, 8, 3, 6, 10, 9, 12, 4].
i=1 (15): çocuklar 1, 2 → 1; index 2'de çocuklar 8, 3 → 3; index 5'te çocuğu 4 → 4; 15 index 10'a iner: [1, 3, 2, 8, 4, 6, 10, 9, 12, 15].

Soru 11zor

Heapsort (max-heap, array indexed from 0, ascending order) is applied to [5, 2, 9, 1, 7, 3]. Show the array after building the max-heap, and after each of the first two deleteMax steps.

Cevabı göster
[9, 7, 5, 1, 2, 3]; [7, 3, 5, 1, 2, 9]; [5, 3, 2, 1, 7, 9]

buildMaxHeap (0-indisli, çocuklar 2i+1, 2i+2), i = 2, 1, 0:
i=2 (9): çocuğu 3, değişmez. i=1 (2): çocuklar 1, 7 → 7 yukarı: [5, 7, 9, 1, 2, 3]. i=0 (5): çocuklar 7, 9 → 9 yukarı; 5 index 2'de, çocuğu 3 küçük: [9, 7, 5, 1, 2, 3].
deleteMax 1: a[0]=9 ile a[5]=3 yer değiştirir, heap boyu 5: [3, 7, 5, 1, 2 | 9]. 3 aşağı: çocuklar 7, 5 → 7; index 1'de çocuklar 1, 2 küçük: [7, 3, 5, 1, 2, 9].
deleteMax 2: 7 ile a[4]=2 değişir, boy 4: [2, 3, 5, 1 | 7, 9]. 2 aşağı: çocuklar 3, 5 → 5; index 2'nin çocuğu yok: [5, 3, 2, 1, 7, 9].
Devamı: [3, 1, 2, 5, 7, 9] → [2, 1, 3, 5, 7, 9] → [1, 2, 3, 5, 7, 9].

Soru 12zor

Write the private method percolateDown(int hole) of a min-heap stored in int[] array (index 1 based) with size elements.

Cevabı göster
Save array[hole]; while hole has a child, pick the smaller child, move it up if it is smaller than the saved value, else stop; put the saved value in the hole

Değeri her adımda takas etmek yerine "delik" (hole) aşağı kaydırılır: daha az atama yapılır.

private void percolateDown(int hole) {
    int tmp = array[hole];
    while (hole * 2 <= size) {            // en az sol cocuk var
        int child = hole * 2;
        if (child != size && array[child + 1] < array[child])
            child++;                       // kucuk cocugu sec
        if (array[child] < tmp)
            array[hole] = array[child];    // cocuk yukari
        else
            break;
        hole = child;
    }
    array[hole] = tmp;
}
Sık hatalar: child != size kontrolünü unutup olmayan sağ çocuğa bakmak, ya da küçük yerine büyük çocuğu seçmek (o max-heap olur).

Doğrulama: Bütün dizi durumları (insert, deleteMin, buildHeap adımları, heapsort ara durumları, max-heap ekleme) ve heap kontrolü Java ile üretildi (verify/SEN2212/W07.java, java W07.java). Sayfadaki MinHeap sınıfı aynı dosyada çalıştırıldı ve elle yazılan ikinci uygulamayla aynı dizileri verdi.

Deftere not kartı

Heap = TAM ikili ağaç (soldan doldurulur) + sıra kuralı min-heap: ebeveyn <= çocuk → en küçük kökte, a[1] 1-indisli dizi: ebeveyn i/2, sol 2i, sağ 2i+1 (a[0] boş) 0-indisli (heapsort): ebeveyn (i-1)/2, sol 2i+1, sağ 2i+2 insert: sona koy, ebeveynden küçükse YUKARI kaydır (percolate up) O(log n) deleteMin: kökü al, SON elemanı köke koy, KÜÇÜK çocukla aşağı kaydır O(log n) buildHeap (Floyd): i = n/2 → 1, her birinde percolateDown → O(n) n kez insert ile kurmak O(n log n) ve FARKLI dizi verebilir yükseklik = floor(log2 n): n=100 → 6 heapsort: max-heap kur, kökü sona takasla, boyu 1 azalt, aşağı kaydır O(n log n) her durumda, ek bellek O(1), kararlı (stable) değil findMin O(1); herhangi bir elemanı arama O(n) (heap BST değil)