Ö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ı.
Ö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:
- İz sürme: “Bu elemanları ekle, iki kez deleteMin yap, diziyi yaz.”
- “What is printed?”:
PriorityQueueile kısa kod. En büyük tuzakSystem.out.println(pq). - 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:
| İşlem | Anlamı | Heap ile maliyet |
|---|---|---|
insert(x) | eleman ekle | |
findMin() | en küçüğü göster | |
deleteMin() | en küçüğü çıkar | |
isEmpty() | boş mu |
Neden heap? Diğer seçeneklerle karşılaştır:
| Yapı | insert | deleteMin |
|---|---|---|
| Sırasız dizi | ||
| Sıralı dizi | ||
| Dengeli BST (AVL) | ||
| Binary heap | , ortalama |
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.
| Metot | Ne yapar | Boşsa |
|---|---|---|
offer(e) / add(e) | ekler | |
poll() | en küçüğü çıkarır ve döndürür | null |
remove() | aynı, ama | NoSuchElementException |
peek() | en küçüğe bakar | null |
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ı istiyorsanpoll()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öntem | Süre |
|---|---|
| Diziyi sırala, sondan k. elemanı al | |
| Hepsini max-heap’e at, k kez poll | (buildHeap ile) |
| Boyutu k olan min-heap tut |
Üçüncüsü veri akışında (stream) da çalışır: veriyi baştan bilmen gerekmez, bellek . 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 . 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 sürer, simüle edilen zaman ne kadar uzun olursa olsun.
Sık yapılan hatalar
| Yanlış düşünce | Doğrusu |
|---|---|
println(pq) sıralı basar | İç diziyi basar; sadece ilk eleman garanti |
| PriorityQueue varsayılan max-heap | Varsayılan min; max için reverseOrder() |
| k. en büyük için max-heap, boyut k | Boyutu k olan min-heap, cevap peek() |
contains hızlıdır, | Heap’te arama yok, |
| Eşit öncelikliler FIFO çıkar | Garanti yok; Comparator’a ikinci kural ekle |
| Java da 1-indeksli | Java 0-indeksli: çocuklar 2i+1, 2i+2 |
| deleteMin’de büyük çocukla değiştir | Min-heap’te küçük çocukla değiştir |
(a, b) -> b - a her zaman güvenli | Taşabilir; Integer.compare(b, a) kullan |
Kendini test et
Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.
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
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.
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
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.
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
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.
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
Önce uzunluğa göre: fig (3), sonra 4 harfliler, en son apple (5). 4 harfliler kendi aralarında alfabetik: date, kiwi, pear, plum.
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
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]
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
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].
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
{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.
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
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.Write a method int kthLargest(int[] a, int k) that returns the k-th largest element using a PriorityQueue in time.
Cevabı göster
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 , n eleman için . {7,2,9,4,11,5,3} için k=1 → 11, k=3 → 7, k=7 → 2.
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
Heap'te en fazla k eleman olur, her işlem . Toplam N kez poll ve en fazla N kez offer yapılır: . Hepsini tek listeye atıp sıralamak olurdu.
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
Ö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.
What is the time complexity of offer, poll, peek and contains on a java.util.PriorityQueue with n elements?
Cevabı göster
offer yukarı süzme, poll aşağı süzme yapar: ağacın yüksekliği kadar, . peek sadece kökü okur: . contains ve remove(Object) diziyi baştan sona tarar: . 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).