Takip et

İkili Yığınlar (Binary Heaps) ve Öncelik Kuyrukları (Priority Queues) JavaScript ile

İkili Yığınlar (Binary Heaps) ve Öncelik Kuyrukları (Priority Queues) JavaScript ile Giriş Bilgisayar bilimlerinde veri yapıları ve algoritm

İ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.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.