İkili Yığınlar (Binary Heaps) ve Öncelik Kuyrukları (Priority Queues) JavaScript ile
Giriş
Bilgisayar bilimlerinde veri yapıları ve algoritmalar, verimli ve ölçeklenebilir yazılım geliştirmek için temel taşlardır. Bu yapıların doğru seçimi, bir uygulamanın performansını dramatik bir şekilde etkileyebilir. Bu makalede, sıkça karşılaşılan ve birçok pratik senaryoda kritik rol oynayan iki önemli veri yapısını ele alacağız: Öncelik Kuyrukları (Priority Queues) ve İkili Yığınlar (Binary Heaps). Özellikle, bu kavramları JavaScript dilinde nasıl uygulayacağımızı ve kullanacağımızı detaylı bir şekilde inceleyeceğiz.
Öncelik kuyrukları, elemanların sıradan bir kuyruktan farklı olarak önceliklerine göre işlendiği soyut bir veri türüdür. Geleneksel kuyruklar “İlk Giren İlk Çıkar” (FIFO) prensibine göre çalışırken, öncelik kuyrukları “En Yüksek Öncelikli İlk Çıkar” prensibini benimser. Bu özellik, işletim sistemlerindeki görev zamanlamadan ağ algoritmalarına, yapay zeka aramalarından simülasyonlara kadar geniş bir uygulama yelpazesinde onları vazgeçilmez kılar.
İkili yığınlar ise, öncelik kuyruklarının en yaygın ve verimli implementasyonlarından biridir. Bir tam ikili ağaç (complete binary tree) yapısına sahip olan ikili yığınlar, elemanların belirli bir “yığın özelliği”ni (heap property) korumasını sağlayarak, en yüksek (veya en düşük) öncelikli elemana hızlı erişim imkanı sunar. Bu makale boyunca, ikili yığınların temel prensiplerini, dizi tabanlı temsilini, temel operasyonlarını ve son olarak JavaScript kullanarak nasıl adım adım oluşturulacağını ve bir öncelik kuyruğu olarak nasıl kullanılacağını detaylandıracağız.
Öncelik Kuyrukları (Priority Queues)
Öncelik kuyruğu, adından da anlaşılacağı gibi, elemanların önceliklerine göre işlendiği özel bir kuyruk türüdür. Normal bir kuyrukta elemanlar giriş sırasına göre (FIFO) işlenirken, öncelik kuyruğunda her elemanla ilişkili bir öncelik değeri bulunur ve en yüksek öncelikli eleman her zaman ilk olarak çıkarılır. Eğer iki elemanın önceliği eşitse, genellikle giriş sırasına göre (veya başka bir ikincil kritere göre) işlenirler.
Temel Operasyonlar
Bir öncelik kuyruğunun tipik olarak desteklediği operasyonlar şunlardır:
* enqueue(element, priority): Kuyruğa belirli bir öncelikle yeni bir eleman ekler.
* dequeue(): Kuyruktan en yüksek öncelikli elemanı çıkarır ve döndürür. (Bazı implementasyonlarda “en düşük” öncelikli de olabilir, bu genellikle “Min-Priority Queue” veya “Max-Priority Queue” olarak adlandırılır.)
* peek(): Kuyruktaki en yüksek öncelikli elemana bakar, ancak onu kuyruktan çıkarmaz.
* isEmpty(): Kuyruğun boş olup olmadığını kontrol eder.
* size(): Kuyruktaki eleman sayısını döndürür.
Kullanım Alanları
Öncelik kuyrukları, bilgisayar bilimleri ve yazılım mühendisliğinde çok çeşitli alanlarda kullanılır:
* İşletim Sistemleri: Görev zamanlayıcılar, hangi sürecin bir sonraki çalışacağını önceliğine göre belirler.
* Ağ Algoritmaları: Dijkstra’nın en kısa yol algoritması ve Prim’in minimum kapsayan ağaç algoritması gibi grafik algoritmaları, ziyaret edilecek bir sonraki düğümü seçmek için öncelik kuyruklarını kullanır.
* Olay Simülasyonları: Gerçek dünya olaylarını simüle ederken, bir sonraki işlenecek olayı zaman önceliğine göre seçmek için kullanılır.
* Veri Sıkıştırma: Huffman kodlama algoritması, karakterlerin frekanslarına göre önceliklerini belirleyerek bir kod ağacı oluşturur.
Yapay Zeka: A arama algoritması gibi en iyi ilk arama algoritmaları, bir sonraki keşfedilecek düğümü önceliğine göre seçmek için öncelik kuyruklarından faydalanır.
Implementasyon Seçenekleri ve Neden İkili Yığınlar?
Öncelik kuyrukları çeşitli şekillerde implemente edilebilir:
* Sıralı Dizi/Bağlı Liste: Elemanları her zaman öncelik sırasına göre tutmak. enqueue işlemi O(N) zaman alır çünkü doğru konumu bulmak ve elemanı eklemek için tüm listenin taranması gerekebilir. dequeue ise O(1) zaman alır çünkü en yüksek öncelikli eleman her zaman baştadır.
* Sırasız Dizi/Bağlı Liste: Elemanları herhangi bir sıraya göre tutmak. enqueue işlemi O(1) zaman alır. Ancak, dequeue işlemi O(N) zaman alır çünkü en yüksek öncelikli elemanı bulmak için tüm listenin taranması gerekir.
Görüldüğü gibi, bu basit implementasyonlar ya enqueue ya da dequeue operasyonlarından birini yavaşlatır. İdeal bir öncelik kuyruğu implementasyonu, her iki operasyonu da verimli bir şekilde gerçekleştirmelidir. İşte bu noktada İkili Yığınlar devreye girer. İkili yığınlar, hem enqueue hem de dequeue operasyonlarını ortalama O(log N) zaman karmaşıklığı ile sunarak, çoğu senaryo için en iyi performansı sağlar.
İkili Yığınlar (Binary Heaps)
İkili yığın, belirli kurallara uyan özel bir tam ikili ağaç (complete binary tree) türüdür. Tam ikili ağaç, son seviye hariç tüm seviyelerin tamamen dolu olduğu ve son seviyedeki düğümlerin mümkün olduğunca solda toplandığı bir ağaçtır. Bu özellik, ikili yığınların bir dizi (array) kullanılarak verimli bir şekilde temsil edilebilmesini sağlar.
Yığın Özelliği (Heap Property)
İkili yığınların en önemli özelliği, “yığın özelliği”dir. Bu özellik, yığının türüne göre değişir:
* Min-Heap (Minimum Yığın): Her ebeveyn düğümün değeri, çocuk düğümlerinin değerinden küçük veya eşit olmalıdır. Bu, yığının kökünün her zaman en küçük değere sahip olmasını sağlar.
* Max-Heap (Maksimum Yığın): Her ebeveyn düğümün değeri, çocuk düğümlerinin değerinden büyük veya eşit olmalıdır. Bu, yığının kökünün her zaman en büyük değere sahip olmasını sağlar.
Bu makalede Min-Heap implementasyonuna odaklanacağız, çünkü genellikle öncelik kuyruklarında “en düşük öncelikli” veya “en erken zamanlı” elemanları çıkarmak için kullanılır. Ancak, mantık Max-Heap için de oldukça benzerdir, sadece karşılaştırma yönleri değişir.
Dizi Temsili
Tam ikili ağaç özelliği sayesinde, bir ikili yığın bir dizi kullanılarak çok verimli bir şekilde temsil edilebilir. Ağaçtaki düğümleri dizinin indekslerine yerleştiririz ve ebeveyn-çocuk ilişkilerini matematiksel formüllerle hesaplarız.
* Kök düğüm dizinin 0. indeksinde bulunur.
* Bir düğüm i indeksinde ise:
* Ebeveyni Math.floor((i - 1) / 2) indeksindedir.
Sol çocuğu 2 i + 1 indeksindedir.
Sağ çocuğu 2 i + 2 indeksindedir.
Bu temsil, işaretçi (pointer) yükünü ortadan kaldırır ve hafıza açısından oldukça verimlidir.
Temel Operasyonlar
Bir ikili yığının temel operasyonları şunlardır:
1. insert(value) (Ekleme):
* Yeni elemanı dizinin sonuna ekleriz.
* Yığın özelliğini korumak için, yeni eklenen elemanı ebeveynleriyle karşılaştırarak yukarı doğru kaydırırız (_bubbleUp veya _heapifyUp). Bu işlem, eleman doğru yerine oturana veya köke ulaşana kadar devam eder.
* Karmaşıklık: O(log N)
2. extractMin() (Minimumu Çıkarma – Min-Heap için):
* Yığının kökü (en küçük eleman) her zaman dizinin 0. indeksindedir. Bu elemanı saklarız.
* Dizinin sonundaki elemanı köke taşırız ve son elemanı diziden çıkarırız.
* Yığın özelliğini korumak için, yeni kök elemanı çocuklarıyla karşılaştırarak aşağı doğru kaydırırız (_sinkDown veya _heapifyDown). Bu işlem, eleman doğru yerine oturana veya bir yaprağa ulaşana kadar devam eder.
* Karmaşıklık: O(log N)
3. peek():
* Yığının kökündeki elemanı döndürür (dizinin 0. indeksindeki eleman).
* Karmaşıklık: O(1)
Şimdi bu operasyonları JavaScript ile nasıl implemente edeceğimize bakalım.
JavaScript ile İkili Yığın Uygulaması (Min-Heap)
Bir MinHeap sınıfı oluşturarak başlayacağız. Bu sınıf, yığın elemanlarını bir JavaScript dizisinde saklayacak ve yığın operasyonlarını gerçekleştiren metotları içerecektir.
class MinHeap {
constructor() {
this.heap = []; // Yığın elemanlarını tutacak dizi
}
// Yığının boyutunu döndürür
size() {
return this.heap.length;
}
// Yığının boş olup olmadığını kontrol eder
isEmpty() {
return this.heap.length === 0;
}
// Elemanları takas etmek için yardımcı metod
_swap(idx1, idx2) {
[this.heap[idx1], this.heap[idx2]] = [this.heap[idx2], this.heap[idx1]];
}
// Bir düğümün ebeveyninin indeksini hesaplar
_getParentIndex(idx) {
return Math.floor((idx - 1) / 2);
}
// Bir düğümün sol çocuğunun indeksini hesaplar
_getLeftChildIndex(idx) {
return 2 * idx + 1;
}
// Bir düğümün sağ çocuğunun indeksini hesaplar
_getRightChildIndex(idx) {
return 2 * idx + 2;
}
// Bir düğümün ebeveynini döndürür
_getParent(idx) {
return this.heap[this._getParentIndex(idx)];
}
// Bir düğümün sol çocuğunu döndürür
_getLeftChild(idx) {
return this.heap[this._getLeftChildIndex(idx)];
}
// Bir düğümün sağ çocuğunu döndürür
_getRightChild(idx) {
return this.heap[this._getRightChildIndex(idx)];
}
// Yığına yeni bir eleman ekler
insert(value) {
this.heap.push(value); // Elemanı dizinin sonuna ekle
this._bubbleUp(); // Yığın özelliğini korumak için yukarı kaydır
}
// En küçük elemanı çıkarır ve döndürür
extractMin() {
if (this.isEmpty()) {
return null;
}
if (this.size() === 1) {
return this.heap.pop(); // Tek eleman varsa direkt onu döndür
}
const min = this.heap[0]; // Kök (en küçük eleman)
this.heap[0] = this.heap.pop(); // Son elemanı köke taşı
this._sinkDown(); // Yığın özelliğini korumak için aşağı kaydır
return min;
}
// Yığının en küçük elemanına bakar (çıkarmaz)
peek() {
if (this.isEmpty()) {
return null;
}
return this.heap[0];
}
// Yeni eklenen elemanı doğru konumuna taşır (yukarı kaydırır)
_bubbleUp() {
let currentIndex = this.size() - 1; // Yeni eklenen elemanın indeksi
// Eleman kök değilse ve ebeveyninden küçükse yukarı kaydır
while (currentIndex > 0 && this.heap[currentIndex] < this._getParent(currentIndex)) {
const parentIndex = this._getParentIndex(currentIndex);
this._swap(currentIndex, parentIndex);
currentIndex = parentIndex; // İndeksi ebeveynin indeksine güncelle
}
}
// Kökten sonra yeni kökü doğru konumuna taşır (aşağı kaydırır)
_sinkDown() {
let currentIndex = 0; // Kökten başla
const length = this.size();
while (true) {
const leftChildIndex = this._getLeftChildIndex(currentIndex);
const rightChildIndex = this._getRightChildIndex(currentIndex);
let smallestChildIndex = currentIndex; // Varsayılan olarak mevcut düğüm en küçük
// Sol çocuk varsa ve mevcut düğümden küçükse
if (leftChildIndex < length && this.heap[leftChildIndex] < this.heap[smallestChildIndex]) {
smallestChildIndex = leftChildIndex;
}
// Sağ çocuk varsa ve (sol çocuktan veya mevcut düğümden) küçükse
if (rightChildIndex < length && this.heap[rightChildIndex] < this.heap[smallestChildIndex]) {
smallestChildIndex = rightChildIndex;
}
// Eğer en küçük çocuk mevcut düğümün kendisi değilse, takas et ve aşağı kaydırmaya devam et
if (smallestChildIndex !== currentIndex) {
this._swap(currentIndex, smallestChildIndex);
currentIndex = smallestChildIndex;
} else {
// Mevcut düğüm doğru yerinde, döngüyü sonlandır
break;
}
}
}
}
Yukarıdaki MinHeap implementasyonu, sayısal değerler için çalışır. Eğer nesneleri önceliklerine göre sıralamak istiyorsak, yığın elemanlarını { value, priority } gibi nesneler olarak saklamamız ve karşılaştırmaları priority alanına göre yapmamız gerekir. Bu, bir sonraki bölümde ele alacağımız PriorityQueue sınıfının temelini oluşturur.
JavaScript ile Öncelik Kuyruğu Uygulaması
Artık bir MinHeap'imiz olduğuna göre, bunu bir PriorityQueue oluşturmak için kullanabiliriz. PriorityQueue, elemanları ve onların önceliklerini yöneten bir arayüz sunacak, arka planda ise bu elemanları bir MinHeap içinde saklayacaktır.
Öncelikle, öncelik kuyruğumuzda saklayacağımız her eleman için bir Node veya Element sınıfı tanımlayabiliriz. Bu, elemanın değerini ve önceliğini bir arada tutmamızı sağlar.
// Öncelik kuyruğundaki her eleman için bir sınıf
class PriorityQueueElement {
constructor(value, priority) {
this.value = value;
this.priority = priority;
}
}
// MinHeap sınıfını PriorityQueue için özelleştirelim.
// Artık sadece sayıları değil, PriorityQueueElement nesnelerini karşılaştıracak.
class PriorityMinHeap {
constructor() {
this.heap = [];
}
size() {
return this.heap.length;
}
isEmpty() {
return this.heap.length === 0;
}
_swap(idx1, idx2) {
[this.heap[idx1], this.heap[idx2]] = [this.heap[idx2], this.heap[idx1]];
}
_getParentIndex(idx) {
return Math.floor((idx - 1) / 2);
}
_getLeftChildIndex(idx) {
return 2 * idx + 1;
}
_getRightChildIndex(idx) {
return 2 * idx + 2;
}
_getParent(idx) {
return this.heap[this._getParentIndex(idx)];
}
// Bu metodlarda artık doğrudan değeri değil, 'priority' özelliğini kullanacağız.
_getLeftChild(idx) {
return this.heap[this._getLeftChildIndex(idx)];
}
_getRightChild(idx) {
return this.heap[this._getRightChildIndex(idx)];
}
insert(element) { // element artık PriorityQueueElement nesnesi
this.heap.push(element);
this._bubbleUp();
}
extractMin() {
if (this.isEmpty()) {
return null;
}
if (this.size() === 1) {
return this.heap.pop();
}
const min = this.heap[0];
this.heap[0] = this.heap.pop();
this._sinkDown();
return min;
}
peek() {
if (this.isEmpty()) {
return null;
}
return this.heap[0];
}
_bubbleUp() {
let currentIndex = this.size() - 1;
while (currentIndex > 0 && this.heap[currentIndex].priority < this._getParent(currentIndex).priority) {
const parentIndex = this._getParentIndex(currentIndex);
this._swap(currentIndex, parentIndex);
currentIndex = parentIndex;
}
}
_sinkDown() {
let currentIndex = 0;
const length = this.size();
while (true) {
const leftChildIndex = this._getLeftChildIndex(currentIndex);
const rightChildIndex = this._getRightChildIndex(currentIndex);
let smallestChildIndex = currentIndex;
// Sol çocuk varsa ve mevcut düğümden daha düşük önceliğe sahipse
if (leftChildIndex < length && this.heap[leftChildIndex].priority < this.heap[smallestChildIndex].priority) {
smallestChildIndex = leftChildIndex;
}
// Sağ çocuk varsa ve (sol çocuktan veya mevcut düğümden) daha düşük önceliğe sahipse
if (rightChildIndex < length && this.heap[rightChildIndex].priority < this.heap[smallestChildIndex].priority) {
smallestChildIndex = rightChildIndex;
}
if (smallestChildIndex !== currentIndex) {
this._swap(currentIndex, smallestChildIndex);
currentIndex = smallestChildIndex;
} else {
break;
}
}
}
}
// Öncelik Kuyruğu sınıfı
class PriorityQueue {
constructor() {
this.minHeap = new PriorityMinHeap(); // Arka planda MinHeap kullanacak
}
// Kuyruğa eleman ekler
enqueue(value, priority) {
const element = new PriorityQueueElement(value, priority);
this.minHeap.insert(element);
}
// En yüksek öncelikli elemanı (yani en düşük priority değerine sahip olanı) çıkarır
dequeue() {
const minElement = this.minHeap.extractMin();
return minElement ? minElement.value : null; // Sadece değeri döndür
}
// En yüksek öncelikli elemana bakar
peek() {
const minElement = this.minHeap.peek();
return minElement ? minElement.value : null; // Sadece değeri döndür
}
// Kuyruğun boş olup olmadığını kontrol eder
isEmpty() {
return this.minHeap.isEmpty();
}
// Kuyruktaki eleman sayısını döndürür
size() {
return this.minHeap.size();
}
}
Bu PriorityQueue sınıfı, PriorityMinHeap'i kullanarak öncelik kuyruğu davranışını sağlar. enqueue metodu, value ve priority ile bir PriorityQueueElement oluşturur ve bunu yığına ekler. dequeue metodu ise yığından en düşük öncelikli elemanı (yani minHeap'teki kökü) çıkarır ve sadece value kısmını döndürür.
Örnek Kullanım Senaryoları
Yukarıdaki PriorityQueue implementasyonumuzu kullanarak birkaç senaryoyu canlandıralım:
1. Görev Zamanlama
Bir işletim sisteminde veya bir sunucu uygulamasında görevleri önceliklerine göre işlemek için kullanılabilir.
const taskQueue = new PriorityQueue();
taskQueue.enqueue("Email Gönder", 3); // Düşük öncelik
taskQueue.enqueue("Veritabanı Yedekle", 1); // Yüksek öncelik
taskQueue.enqueue("Kullanıcı Arayüzü Güncelle", 2); // Orta öncelik
taskQueue.enqueue("Sistem Loglarını Temizle", 4); // En düşük öncelik
taskQueue.enqueue("Kritik Hata İşle", 0); // En yüksek öncelik
console.log("Görevler öncelik sırasına göre işleniyor:");
while (!taskQueue.isEmpty()) {
const nextTask = taskQueue.dequeue();
console.log(İşleniyor: ${nextTask});
}
// Çıktı:
// İşleniyor: Kritik Hata İşle
// İşleniyor: Veritabanı Yedekle
// İşleniyor: Kullanıcı Arayüzü Güncelle
// İşleniyor: Email Gönder
// İşleniyor: Sistem Loglarını Temizle
2. Olay Sıralama (Simülasyonlar için)
Bir simülasyonda, olayların gerçekleşme zamanlarına göre sıralanması gerekebilir. Burada priority alanını olay zamanı olarak kullanabiliriz.
const eventQueue = new PriorityQueue();
eventQueue.enqueue("Kullanıcı A girişi", 100);
eventQueue.enqueue("Kullanıcı B çıkışı", 150);
eventQueue.enqueue("Sunucu hatası", 50); // Daha erken bir olay
eventQueue.enqueue("Veri senkronizasyonu", 120);
console.log("\nOlaylar zaman sırasına göre işleniyor:");
while (!eventQueue.isEmpty()) {
const nextEvent = eventQueue.dequeue();
console.log(Olay: ${nextEvent});
}
// Çıktı:
// Olay: Sunucu hatası
// Olay: Kullanıcı A girişi
// Olay: Veri senkronizasyonu
// Olay: Kullanıcı B çıkışı
Bu örnekler, öncelik kuyruklarının gerçek dünya problemlerini çözmek için ne kadar güçlü ve esnek bir araç olduğunu göstermektedir.
Karmaşıklık Analizi
Bir veri yapısının verimliliğini değerlendirirken, operasyonlarının zaman ve alan karmaşıklığını incelemek önemlidir. İkili yığın tabanlı bir öncelik kuyruğu için karmaşıklık analizi şöyledir:
Zaman Karmaşıklığı (Time Complexity)
* enqueue(value, priority): Yeni bir eleman ekleme işlemi. Eleman dizinin sonuna eklenir ve ardından _bubbleUp işlemi ile doğru yerine kaydırılır. Bir elemanı yığında yukarı kaydırmak, yığının yüksekliği kadar adım gerektirir. Bir ikili yığının yüksekliği O(log N)'dir (burada N, yığındaki eleman sayısıdır). Dolayısıyla, enqueue işlemi O(log N) zaman alır.
* dequeue(): En yüksek öncelikli elemanı (Min-Heap için en küçük) çıkarma işlemi. Kök eleman O(1) zamanda alınır. Dizinin son elemanı köke taşınır ve ardından _sinkDown işlemi ile doğru yerine kaydırılır. Bir elemanı yığında aşağı kaydırmak da yığının yüksekliği kadar adım gerektirir. Bu nedenle, dequeue işlemi O(log N) zaman alır.
* peek(): En yüksek öncelikli elemana bakma işlemi. Sadece dizinin ilk elemanını döndürmeyi gerektirdiğinden, O(1) zaman alır.
* isEmpty() / size(): Dizinin uzunluğunu kontrol etmek veya döndürmek O(1) zaman alır.
Bu logaritmik zaman karmaşıklıkları, yığın tabanlı öncelik kuyruklarını büyük veri kümeleriyle çalışırken bile oldukça verimli kılar. Diğer basit implementasyonlara kıyasla (dizi tabanlı sıralı veya sırasız), yığınlar hem ekleme hem de çıkarma işlemlerinde dengeli ve iyi bir performans sunar.
Alan Karmaşıklığı (Space Complexity)
* Bir ikili yığın, elemanlarını bir dizi içinde saklar. Yığındaki N eleman için, dizi de N eleman tutar. Bu nedenle, alan karmaşıklığı O(N)'dir. Bu, işaretçi tabanlı ağaç yapılarına göre daha hafıza dostu olabilir çünkü her düğüm için ek işaretçi (sol çocuk, sağ çocuk, ebeveyn) depolama maliyeti yoktur.
Avantajları ve Dezavantajları
Avantajları
* Verimli Operasyonlar: Hem enqueue hem de dequeue operasyonları için O(log N) zaman karmaşıklığı sunar, bu da büyük veri kümeleri için idealdir.
* Hafıza Verimliliği: Dizi tabanlı implementasyonu sayesinde, işaretçi yükü olmadan kompakt bir bellek kullanımı sağlar.
* Basit ve Anlaşılır: Yığın yapısı ve operasyonları nispeten basit ve anlaşılırdır, bu da implementasyon ve hata ayıklamayı kolaylaştırır.
* Geniş Uygulama Alanı: İşletim sistemleri, ağ algoritmaları, simülasyonlar, yapay zeka gibi birçok alanda temel bir yapı taşıdır.
Dezavantajları
* Rastgele Erişimin Yokluğu: Yığın, sıralı bir dizi olmamasına rağmen, rastgele bir elemana indeksine göre erişmek O(N) zaman alır. Sadece kök elemana (en yüksek/en düşük öncelikli) hızlı erişim mümkündür.
* delete ve update İşlemlerinin Zorluğu: Yığından rastgele bir elemanı silmek veya bir elemanın önceliğini güncellemek, yığın özelliğini bozabilir. Bu tür işlemler genellikle elemanı bulmayı (O(N)), silmeyi ve yığını yeniden düzenlemeyi (O(log N)) veya tüm yığını yeniden inşa etmeyi (O(N)) gerektirir. Bu, yığının temel güçlü yönlerinden biri değildir.
* Sadece En Yüksek/Düşük Öncelikli Erişimi: Yığınlar, sadece en yüksek veya en düşük öncelikli elemana hızlı erişim sağlamak üzere tasarlanmıştır. Orta öncelikli elemanları bulmak veya çıkarmak verimli değildir.
Sonuç
Bu makalede, veri yapıları dünyasının iki önemli kavramı olan İkili Yığınlar ve Öncelik Kuyruklarını detaylı bir şekilde inceledik. Öncelik kuyruklarının, elemanları önceliklerine göre işleme yeteneği sayesinde birçok gerçek dünya probleminde vazgeçilmez bir araç olduğunu gördük. İkili yığınların ise, bu öncelik kuyruklarının en verimli ve yaygın implementasyonlarından biri olduğunu, tam ikili ağaç özelliği ve dizi tabanlı temsil sayesinde O(log N) zaman karmaşıklığı ile temel operasyonları gerçekleştirebildiğini öğrendik.
JavaScript ile adım adım bir MinHeap ve bu MinHeap'i kullanan bir PriorityQueue sınıfını nasıl oluşturabileceğimizi gösterdik. Bu implementasyonlar, JavaScript geliştiricilerinin modern uygulamalarda performans gerektiren senaryoları ele almalarına olanak tanır. İster görev zamanlama, ister ağ rotalama, isterse de oyun motorlarında olay yönetimi olsun, ikili yığınlar ve öncelik kuyrukları, yazılım mühendisliği araç kutunuzda bulunması gereken temel veri yapılarıdır.
Bu makalede ele alınan kavramlar, veri yapıları ve algoritmalar dünyasına bir giriş niteliğindedir. Daha karmaşık öncelik kuyruğu implementasyonları (örneğin Fibonacci Yığınları veya Binomial Yığınları) veya yığınların diğer algoritmalarla (örneğin Heapsort) entegrasyonu gibi konular, bu temel bilgilerin üzerine inşa edilebilir. Bu temel anlayış, daha verimli ve ölçeklenebilir JavaScript uygulamaları geliştirmenize yardımcı olacaktır.