SEN2212 · 8. hafta · Using heaps as priority queues. Midterm.

Öncelik kuyruğu: heap ile PriorityQueue

Binary heap'in öncelik kuyruğu olarak kullanımı, Java'nın PriorityQueue sınıfı, Comparator ile sıralama ve k-en-küçük, k listeyi birleştirme, iş zamanlama uygulamaları.

priority queuebinary heapPriorityQueueofferpollpeekComparatorCollections.reverseOrderk-th largestk-way mergeevent-driven simulationmin-heapmax-heap

Öncelik kuyruğu (priority queue) sıradan kuyruğun “ilk gelen ilk çıkar” kuralını bırakır. Her eleman bir öncelik taşır ve her seferinde en öncelikli olan çıkar. Hastanenin acil servisi, işletim sisteminin işlem zamanlayıcısı, Dijkstra algoritması hep bunu kullanır. Geçen hafta binary heap’i bir veri yapısı olarak gördün. Bu hafta onu bir soyut veri tipinin (ADT) gerçekleştirmesi olarak kullanıyoruz ve Java’nın hazır PriorityQueue sınıfını tanıyoruz.

Sınavda bu konudan üç tip soru gelir:

  1. İz sürme: “Bu elemanları ekle, iki kez deleteMin yap, diziyi yaz.”
  2. “What is printed?”: PriorityQueue ile kısa kod. En büyük tuzak System.out.println(pq).
  3. Uygulama + Big-O: “k. en büyüğü bul”, “k listeyi birleştir”, hangi heap ve hangi karmaşıklık.

Öncelik kuyruğu ADT’si

İşlemler:

İşlemAnlamıHeap ile maliyet
insert(x)eleman ekleO(log⁡n)O(\log n)
findMin()en küçüğü gösterO(1)O(1)
deleteMin()en küçüğü çıkarO(log⁡n)O(\log n)
isEmpty()boş muO(1)O(1)

Neden heap? Diğer seçeneklerle karşılaştır:

YapıinsertdeleteMin
Sırasız diziO(1)O(1)O(n)O(n)
Sıralı diziO(n)O(n)O(1)O(1)
Dengeli BST (AVL)O(log⁡n)O(\log n)O(log⁡n)O(\log n)
Binary heapO(log⁡n)O(\log n), ortalama O(1)O(1)O(log⁡n)O(\log n)

AVL de aynı Big-O’yu verir ama işaretçilerle uğraşır, rotasyon yapar. Heap tek bir dizidir, işaretçi yoktur, kod kısadır. Öncelik kuyruğu için standart seçim budur.

Hatırlatma: 1-indeksli min-heap

Ders kitabının kullandığı gösterim: kök array[1], i indeksindeki düğümün çocukları 2i ve 2i+1, ebeveyni i/2. Heap-order kuralı: her ebeveyn çocuklarından küçük ya da eşittir.

insert (percolate up): Elemanı dizinin sonuna koy. Ebeveyninden küçük olduğu sürece yer değiştir.

deleteMin (percolate down): Kökü çıkar. Son elemanı köke taşı. Küçük çocuğundan büyük olduğu sürece küçük çocukla yer değiştir.

Örnek: boş heap’e 10, 4, 7, 1, 8, 3 ekle.

insert 10 -> [10]
insert 4  -> [4, 10]
insert 7  -> [4, 10, 7]
insert 1  -> [1, 4, 7, 10]
insert 8  -> [1, 4, 7, 10, 8]
insert 3  -> [1, 4, 3, 10, 8, 7]

Son hali ağaç olarak:

          1
        /   \
       4     3
      / \   /
    10   8 7

Şimdi iki kez deleteMin:

deleteMin 1 -> [3, 4, 7, 10, 8]
deleteMin 3 -> [4, 8, 7, 10]

İkincisini açalım. 3 çıkar, son eleman 8 köke gelir. Çocuklar 4 ve 7, küçüğü 4 ve 4 < 8, yer değiştir. 8 artık indeks 2’de, tek çocuğu 10 (indeks 4). 10 > 8, dur.

TUZAK · deleteMin’de büyük çocukla değiştirmek. Min-heap’te aşağı süzerken küçük çocukla yer değiştirirsin. Büyükle değiştirirsen küçük çocuk yeni ebeveyninden küçük kalır ve heap kuralı bozulur.

Java’da PriorityQueue

java.util.PriorityQueue<E> bir binary heap’tir. İki fark var: dizi 0-indekslidir (ebeveyn (i-1)/2, çocuklar 2i+1, 2i+2) ve varsayılan olarak min-heaptir.

MetotNe yaparBoşsa
offer(e) / add(e)ekler
poll()en küçüğü çıkarır ve döndürürnull
remove()aynı, amaNoSuchElementException
peek()en küçüğe bakarnull
size(), isEmpty()
PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int x : new int[]{5, 1, 8, 3, 2}) pq.offer(x);
System.out.println(pq);            // iç dizi!
for (int x : pq) System.out.print(x + " ");
System.out.println();
while (!pq.isEmpty()) System.out.print(pq.poll() + " ");
[1, 2, 8, 5, 3]
1 2 8 5 3 
1 2 3 5 8 

TUZAK · println(pq) sıralı değildir. toString() ve for-each döngüsü heap’in iç dizisini sırayla dolaşır. Sadece ilk elemanın en küçük olduğu garantidir. Sıralı çıktı istiyorsan poll() döngüsü kullan. Sınavda [1, 2, 3, 5, 8] yazan puanı kaybeder.

Max-heap ve Comparator

Max-heap için ters sıralayıcı ver:

PriorityQueue<Integer> mx = new PriorityQueue<>(Collections.reverseOrder());
for (int x : new int[]{5, 1, 8, 3, 2}) mx.offer(x);
System.out.println(mx.peek() + " " + mx);
8 [8, 3, 5, 1, 2]

Kendi sıralama kuralın için Comparator ver. Uzunluğa göre, eşitse alfabetik:

PriorityQueue<String> ps = new PriorityQueue<>(
    Comparator.comparingInt(String::length)
              .thenComparing(Comparator.naturalOrder()));
for (String s : new String[]{"pear","fig","apple","kiwi","date","plum"})
    ps.offer(s);
while (!ps.isEmpty()) System.out.print(ps.poll() + " ");
fig date kiwi pear plum apple 

Lambda ile de yazılır: new PriorityQueue<>((a, b) -> b - a) max-heap verir. Ama b - a çok büyük sayılarda taşabilir (overflow); Integer.compare(b, a) daha güvenlidir.

TUZAK · PriorityQueue kararlı değildir. Eşit öncelikli iki eleman eklenme sırasıyla çıkmak zorunda değil. FIFO istiyorsan Comparator’a ikinci kural ekle: ekleme sıra numarası ya da isim.

Uygulama 1: k. en büyük ve k en küçük

n elemanın k. en büyüğünü bulmanın üç yolu:

YöntemSüre
Diziyi sırala, sondan k. elemanı alO(nlog⁡n)O(n \log n)
Hepsini max-heap’e at, k kez pollO(n+klog⁡n)O(n + k \log n) (buildHeap ile)
Boyutu k olan min-heap tutO(nlog⁡k)O(n \log k)

Üçüncüsü veri akışında (stream) da çalışır: veriyi baştan bilmen gerekmez, bellek O(k)O(k). Fikir: heap’te o ana kadarki en büyük k eleman durur. Kök bunların en küçüğüdür. Yeni eleman gelince ekle, boyut k’yı geçerse kökü at.

static int kthLargest(int[] a, int k) {
    PriorityQueue<Integer> pq = new PriorityQueue<>();
    for (int x : a) {
        pq.offer(x);
        if (pq.size() > k) pq.poll();
    }
    return pq.peek();
}

{7, 2, 9, 4, 11, 5, 3} ve k = 3 için izle:

oku 7  -> {7}
oku 2  -> {2,7}
oku 9  -> {2,7,9}
oku 4  -> {2,4,7,9}  boyut 4 > 3, at 2 -> {4,7,9}
oku 11 -> {4,7,9,11} at 4 -> {7,9,11}
oku 5  -> {5,7,9,11} at 5 -> {7,9,11}
oku 3  -> {3,7,9,11} at 3 -> {7,9,11}
cevap peek = 7

Simetrik kural: k en küçük için max-heap, k en büyük için min-heap. Heap’in kökü “kapıdaki bekçi”dir; içeri girmeye aday olanı onunla karşılaştırırsın.

Uygulama 2: k sıralı listeyi birleştirmek

Merge sort’ta iki listeyi birleştirmeyi biliyorsun. k liste olunca her adımda k başın en küçüğünü bulmak gerekir. Bunu heap yapar: her listenin ilk elemanını heap’e koy. Poll ettiğin eleman hangi listeden geldiyse o listenin bir sonraki elemanını ekle.

// heap elemanı: {değer, listeNo, indeks}
PriorityQueue<int[]> pq =
    new PriorityQueue<>((p, q) -> Integer.compare(p[0], q[0]));
for (int i = 0; i < lists.size(); i++)
    if (!lists.get(i).isEmpty())
        pq.offer(new int[]{lists.get(i).get(0), i, 0});
List<Integer> out = new ArrayList<>();
while (!pq.isEmpty()) {
    int[] t = pq.poll();
    out.add(t[0]);
    int j = t[2] + 1;
    if (j < lists.get(t[1]).size())
        pq.offer(new int[]{lists.get(t[1]).get(j), t[1], j});
}

[1,5,9], [2,6], [3,4,10] için çıktı:

[1, 2, 3, 4, 5, 6, 9, 10]

Heap’te hiçbir zaman k’dan fazla eleman yoktur. N toplam eleman için süre O(Nlog⁡k)O(N \log k). Büyük dosyaları parça parça sıralayıp birleştiren harici sıralama (external sorting) bu fikre dayanır.

Uygulama 3: zamanlama ve olay simülasyonu

İş zamanlama (scheduling): İşletim sistemi bekleyen işleri önceliğe göre tutar. Her seferinde en öncelikli iş çalışır. Comparator’da önce öncelik, eşitse isim ya da geliş sırası:

record Job(String name, int prio) {}
PriorityQueue<Job> jq = new PriorityQueue<>(
    Comparator.comparingInt(Job::prio).thenComparing(Job::name));
jq.offer(new Job("C", 2)); jq.offer(new Job("A", 3));
jq.offer(new Job("D", 1)); jq.offer(new Job("B", 1));
while (!jq.isEmpty()) System.out.print(jq.poll().name() + " ");
B D C A 

Olay güdümlü simülasyon (event-driven simulation): Bir banka gişesini simüle ettiğini düşün. Saniye saniye ilerlemek yerine olayları (“müşteri geldi”, “müşteri ayrıldı”) zamanlarına göre bir min-heap’e koyarsın. Her adımda en erken olayı çekersin, saat doğrudan o ana atlar. Olay yeni olay doğurabilir: “geldi” olayı işlenince hizmet bitiş zamanında bir “ayrıldı” olayı eklenir. Üç müşteri t = 0, 1, 2’de gelsin, tek gişe, hizmet 4 dakika:

t=0  arrive0  -> leave0 olayı t=4'e eklenir
t=1  arrive1  -> gişe 4'e kadar dolu, leave1 t=8
t=2  arrive2  -> leave2 t=12
t=4  leave0
t=8  leave1
t=12 leave2

Olay sayısı E ise simülasyon O(Elog⁡E)O(E \log E) sürer, simüle edilen zaman ne kadar uzun olursa olsun.

Sık yapılan hatalar

Yanlış düşünceDoğrusu
println(pq) sıralı basarİç diziyi basar; sadece ilk eleman garanti
PriorityQueue varsayılan max-heapVarsayılan min; max için reverseOrder()
k. en büyük için max-heap, boyut kBoyutu k olan min-heap, cevap peek()
contains hızlıdır, O(log⁡n)O(\log n)Heap’te arama yok, O(n)O(n)
Eşit öncelikliler FIFO çıkarGaranti yok; Comparator’a ikinci kural ekle
Java da 1-indeksliJava 0-indeksli: çocuklar 2i+1, 2i+2
deleteMin’de büyük çocukla değiştirMin-heap’te küçük çocukla değiştir
(a, b) -> b - a her zaman güvenliTaşabilir; Integer.compare(b, a) kullan

Kendini test et

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

Soru 1kolay

What is printed?

PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int x : new int[]{5, 1, 8, 3, 2}) pq.offer(x);
while (!pq.isEmpty()) System.out.print(pq.poll() + " ");
Cevabı göster
1 2 3 5 8

PriorityQueue varsayılan olarak min-heap'tir. poll() her seferinde en küçüğü çıkarır, bu yüzden elemanlar artan sırada gelir: 1 2 3 5 8.

Soru 2orta

What is printed?

PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int x : new int[]{5, 1, 8, 3, 2}) pq.offer(x);
System.out.println(pq);
Cevabı göster
[1, 2, 8, 5, 3]

toString() sıralı liste basmaz, heap'in iç dizisini (0-indeksli) olduğu gibi basar. Adım adım:
offer 5 → [5]
offer 1 → sona eklenir, ebeveyn 5'ten küçük, yukarı çıkar → [1, 5]
offer 8 → [1, 5, 8] (ebeveyn 1, yer değişmez)
offer 3 → indeks 3, ebeveyn indeks 1 (5). 3 < 5, yer değişir → [1, 3, 8, 5]; ebeveyn 1, dur.
offer 2 → indeks 4, ebeveyn indeks 1 (3). 2 < 3, değişir → [1, 2, 8, 5, 3]; ebeveyn 1, dur.

Soru 3kolay

What is printed?

PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder());
for (int x : new int[]{5, 1, 8, 3, 2}) pq.offer(x);
pq.poll(); pq.poll();
System.out.println(pq.peek() + " " + pq.size());
Cevabı göster
3 3

Collections.reverseOrder() kuyruğu max-heap yapar. İlk iki poll() 8 ve 5'i çıkarır. Geriye {1, 2, 3} kalır, en büyüğü 3, boyut 3.

Soru 4orta

What is printed?

PriorityQueue<String> pq = new PriorityQueue<>(
    Comparator.comparingInt(String::length)
              .thenComparing(Comparator.naturalOrder()));
for (String s : new String[]{"pear","fig","apple","kiwi","date","plum"})
    pq.offer(s);
while (!pq.isEmpty()) System.out.print(pq.poll() + " ");
Cevabı göster
fig date kiwi pear plum apple

Önce uzunluğa göre: fig (3), sonra 4 harfliler, en son apple (5). 4 harfliler kendi aralarında alfabetik: date, kiwi, pear, plum.

Soru 5orta

Insert 10, 4, 7, 1, 8, 3 (in this order) into an initially empty binary min-heap stored in an array starting at index 1. Give the array after all insertions.

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

Her ekleme sona yapılır, sonra yukarı süzülür (percolate up). Ebeveyn = i/2.
10 → [10]
4 → 4 < 10, yukarı → [4, 10]
7 → ebeveyn 4, dur → [4, 10, 7]
1 → indeks 4, ebeveyn 10 → değiş; indeks 2, ebeveyn 4 → değiş → [1, 4, 7, 10]
8 → indeks 5, ebeveyn 4, dur → [1, 4, 7, 10, 8]
3 → indeks 6, ebeveyn 7 → değiş; indeks 3, ebeveyn 1, dur → [1, 4, 3, 10, 8, 7]

Soru 6orta

Starting from the heap [1, 4, 3, 10, 8, 7] (1-indexed min-heap), perform deleteMin twice. Give the array after each operation.

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

1. deleteMin: 1 çıkar, son eleman 7 köke gelir. Çocuklar 4 ve 3, küçük olan 3 < 7 → 3 yukarı, delik indeks 3'e. İndeks 3'ün çocuğu yok (n=5) → 7 oraya: [3, 4, 7, 10, 8].
2. deleteMin: 3 çıkar, son eleman 8 köke. Çocuklar 4 ve 7, küçük 4 < 8 → 4 yukarı, delik indeks 2. Çocuğu 10 (indeks 4), 10 > 8 → dur, 8 indeks 2'ye: [4, 8, 7, 10].

Soru 7kolay

What is printed?

PriorityQueue<Integer> m = new PriorityQueue<>();
m.offer(6); m.offer(2); m.offer(9);
int a = m.poll();
m.offer(1); m.offer(7);
int b = m.poll();
System.out.println(a + " " + b + " " + m.peek() + " " + m.size());
Cevabı göster
2 1 6 3

{6,2,9} → poll 2, kalan {6,9}. 1 ve 7 eklenir → {1,6,7,9}. poll 1, kalan {6,7,9}. peek 6, size 3.

Soru 8orta

Using a min-heap of size k = 3, find the 3rd largest element of {7, 2, 9, 4, 11, 5, 3}. Which elements remain in the heap at the end, and what is the answer?

Cevabı göster
heap = {7, 9, 11}; answer 7

Her elemanı ekle, boyut 3'ü geçince en küçüğü at.

7,2,9 → {2,7,9}
4 ekle → {2,4,7,9}, 2 atılır → {4,7,9}
11 → {4,7,9,11}, 4 atılır → {7,9,11}
5 → 5 eklenip hemen atılır → {7,9,11}
3 → aynı şekilde atılır → {7,9,11}
Heap'te en büyük 3 eleman kalır. Kök (en küçükleri) 3. en büyüktür: 7.

Soru 9zor

Write a method int kthLargest(int[] a, int k) that returns the k-th largest element using a PriorityQueue in O(nlog⁡k)O(n \log k) time.

Cevabı göster
Size-k min-heap; return peek()

Min-heap'i k boyutunda tut. Boyut k'yı geçince en küçüğü at; sonunda kök cevaptır.

static int kthLargest(int[] a, int k) {
    PriorityQueue<Integer> pq = new PriorityQueue<>();
    for (int x : a) {
        pq.offer(x);
        if (pq.size() > k) pq.poll();
    }
    return pq.peek();
}

Her offer/poll O(log⁡k)O(\log k), n eleman için O(nlog⁡k)O(n \log k). {7,2,9,4,11,5,3} için k=1 → 11, k=3 → 7, k=7 → 2.

Soru 10orta

You merge k sorted lists containing N elements in total, using a min-heap that holds one element from each list. What is the running time?

Cevabı göster
O(N log k)

Heap'te en fazla k eleman olur, her işlem O(log⁡k)O(\log k). Toplam N kez poll ve en fazla N kez offer yapılır: O(Nlog⁡k)O(N \log k). Hepsini tek listeye atıp sıralamak O(Nlog⁡N)O(N \log N) olurdu.

Soru 11zor

Jobs are inserted into a PriorityQueue<Job> with comparator comparingInt(Job::prio).thenComparing(Job::name) (smaller prio = served first). Insert order: C(2), A(3), D(1), B(1). In what order are they polled?

Cevabı göster
B D C A

Önce öncelik: prio 1 olanlar (D ve B), sonra C (2), en son A (3). D ile B eşit öncelikte, ikinci kural isim: B < D. Sıra B D C A. İkinci kural olmasaydı eşitlerin sırası garanti değildi; PriorityQueue kararlı (stable) değildir.

Soru 12kolay

What is the time complexity of offer, poll, peek and contains on a java.util.PriorityQueue with n elements?

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

offer yukarı süzme, poll aşağı süzme yapar: ağacın yüksekliği kadar, O(log⁡n)O(\log n). peek sadece kökü okur: O(1)O(1). contains ve remove(Object) diziyi baştan sona tarar: O(n)O(n). Heap arama için tasarlanmamıştır.

Doğrulama: Tüm heap dizileri, PriorityQueue çıktıları (toString, for-each, poll sırası) ve uygulama sonuçları JDK 26 ile çalıştırıldı (verify/SEN2212/W08.java, java W08.java).

Deftere not kartı

Öncelik kuyruğu: insert + deleteMin. Heap ile ikisi de O(log n), findMin O(1). 1-indeksli: ebeveyn i/2, çocuklar 2i ve 2i+1. Java'nın PriorityQueue'su 0-indeksli: ebeveyn (i-1)/2. PriorityQueue varsayılan MIN-heap. Max için new PriorityQueue<>(Collections.reverseOrder()). offer/poll O(log n), peek O(1), contains/remove(obj) O(n). System.out.println(pq) ve for-each SIRALI DEĞİL, iç diziyi basar. Sıralı istiyorsan poll döngüsü. Comparator: comparingInt(...).thenComparing(...). Eşitlerde sıra garanti değil (stable değil). k. en büyük: boyutu k olan MIN-heap, taşınca poll; cevap peek. O(n log k). k en küçük: boyutu k olan MAX-heap. k sıralı listeyi birleştir: her listeden bir eleman heap'e, poll edince aynı listeden sonrakini ekle. O(N log k). Olay simülasyonu: olaylar zamana göre heap'te, hep en erken olay işlenir. poll() boşsa null döner, remove() exception atar. peek() boşsa null.