🎯 9.6: Heap Örnekleri ve Problemler

Heap Veri Yapısıyla Çözülen Klasik Problemler

1. En Büyük K Eleman (Top K Elements) Kolay

Bir nums dizisi ve k sayısı verildiğinde, dizideki en büyük k elemanı döndürün.

Örnek:
Input: nums = [3, 1, 5, 12, 2, 11], k = 3
Output: [12, 11, 5]
💡 İpucu: Python'un heapq.nlargest() fonksiyonu doğrudan kullanılabilir. Ya da min heap ile sadece k eleman tutarak çözün.

🧠 Düşünce Süreci

  1. Naive: Sırala, son k elemanı al → O(n log n)
  2. Heap: Min heap ile sadece k eleman tut → O(n log k)
  3. Yeni eleman heap'in min'inden büyükse, min'i çıkar yeniyi ekle
Çözüm: Top K Elements
import heapq

def top_k_elements(nums, k):
    """En büyük k elemanı döndür - O(n log k)"""
    # Min heap ile sadece k eleman tut
    min_heap = []

    for num in nums:
        heapq.heappush(min_heap, num)
        # k'dan fazla varsa en küçüğü çıkar
        if len(min_heap) > k:
            heapq.heappop(min_heap)

    # Heap'teki k eleman = en büyük k eleman
    return sorted(min_heap, reverse=True)

def top_k_simple(nums, k):
    """Basit çözüm - heapq.nlargest kullan"""
    return heapq.nlargest(k, nums)

# Test
nums = [3, 1, 5, 12, 2, 11, 8, 9]
k = 3

print(f"Dizi: {nums}")
print(f"k = {k}")
print(f"\nEn büyük {k} eleman (heap):", top_k_elements(nums, k))
print(f"En büyük {k} eleman (nlargest):", top_k_simple(nums, k))
Çıktı bekleniyor...
⏱️ Karmaşıklık:
Zaman: O(n log k) - her eleman için O(log k) heap işlemi
Alan: O(k) - sadece k eleman tutuyoruz
2. Kth En Büyük Eleman Kolay

Bir dizide k. en büyük elemanı bulun. (LeetCode #215)

Örnek:
Input: nums = [3, 2, 1, 5, 6, 4], k = 2
Output: 5 (En büyük: 6, 2. en büyük: 5)

🧠 Düşünce Süreci

  1. Min heap ile k eleman tut
  2. Döngü sonunda heap'in root'u = k. en büyük
Çözüm: Kth Largest
import heapq

def find_kth_largest(nums, k):
    """K. en büyük elemanı bul - O(n log k)"""
    # Min heap ile k eleman tut
    min_heap = []

    for num in nums:
        heapq.heappush(min_heap, num)
        if len(min_heap) > k:
            heapq.heappop(min_heap)

    # Heap root = k. en büyük
    return min_heap[0]

def find_kth_largest_simple(nums, k):
    """Basit: heapq.nlargest kullan"""
    return heapq.nlargest(k, nums)[-1]

# Test
test_cases = [
    ([3, 2, 1, 5, 6, 4], 2),  # 5
    ([3, 2, 3, 1, 2, 4, 5, 5, 6], 4),  # 4
    ([1], 1),  # 1
]

for nums, k in test_cases:
    result = find_kth_largest(nums, k)
    print(f"nums={nums}, k={k} → {k}. en büyük: {result}")
Çıktı bekleniyor...
⏱️ Karmaşıklık: O(n log k) zaman, O(k) alan
3. K Sıralı Listeyi Birleştir Orta

K adet sıralı liste verildiğinde, bunları tek bir sıralı listede birleştirin. (LeetCode #23)

Örnek:
Input: [[1,4,5], [1,3,4], [2,6]]
Output: [1,1,2,3,4,4,5,6]
💡 İpucu: Min heap'e her listenin ilk elemanını koy. Pop edince o listeden sonraki elemanı ekle.

🧠 Düşünce Süreci

  1. Min heap kullan → her zaman en küçük elemanı al
  2. Heap'e (değer, liste_idx, eleman_idx) tuple ekle
  3. Pop edince, o listeden sonraki elemanı heap'e push et
Çözüm: Merge K Sorted Lists
import heapq

def merge_k_sorted(lists):
    """K sıralı listeyi birleştir - O(n log k)"""
    min_heap = []
    result = []

    # Her listenin ilk elemanını heap'e ekle
    for i, lst in enumerate(lists):
        if lst:  # Boş liste değilse
            # (değer, liste_index, eleman_index)
            heapq.heappush(min_heap, (lst[0], i, 0))

    while min_heap:
        val, list_idx, elem_idx = heapq.heappop(min_heap)
        result.append(val)

        # Bu listede sonraki eleman varsa ekle
        if elem_idx + 1 < len(lists[list_idx]):
            next_val = lists[list_idx][elem_idx + 1]
            heapq.heappush(min_heap, (next_val, list_idx, elem_idx + 1))

    return result

# Test
lists = [
    [1, 4, 5],
    [1, 3, 4],
    [2, 6]
]

print("Giriş listeleri:")
for i, lst in enumerate(lists):
    print(f"  Liste {i+1}: {lst}")

print(f"\nBirleştirilmiş: {merge_k_sorted(lists)}")

# Edge case test
print(f"\nBoş listelerle: {merge_k_sorted([[], [1], [2,3]])}")
Çıktı bekleniyor...
⏱️ Karmaşıklık:
Zaman: O(n log k) - n toplam eleman, heap boyutu k
Alan: O(k) - heap için
4. En Yakın K Nokta Orta

2D düzlemde noktalar verildiğinde, orijine (0,0) en yakın k noktayı bulun. (LeetCode #973)

Örnek:
Input: points = [[1,3], [-2,2], [5,8], [0,1]], k = 2
Output: [[0,1], [-2,2]] (uzaklıklar: 1, √8 ≈ 2.83)

🧠 Düşünce Süreci

  1. Uzaklık = √(x² + y²), ama karşılaştırma için x² + y² yeterli
  2. Max heap (negatif uzaklık) ile k eleman tut
  3. Ya da min heap ile tüm noktaları koyup k kez pop
Çözüm: K Closest Points
import heapq

def k_closest(points, k):
    """Orijine en yakın k noktayı bul - O(n log k)"""
    # Max heap (negatif uzaklık) ile k eleman tut
    max_heap = []

    for x, y in points:
        dist = x*x + y*y  # Kare uzaklık (sqrt gerekmez)

        # Max heap için negatif kullan
        heapq.heappush(max_heap, (-dist, [x, y]))

        # k'dan fazla varsa en uzağı çıkar
        if len(max_heap) > k:
            heapq.heappop(max_heap)

    return [point for _, point in max_heap]

def k_closest_simple(points, k):
    """Alternatif: nsmallest ile"""
    return heapq.nsmallest(k, points, key=lambda p: p[0]**2 + p[1]**2)

# Test
points = [[1, 3], [-2, 2], [5, 8], [0, 1]]
k = 2

print(f"Noktalar: {points}")
print(f"k = {k}")
print(f"\nEn yakın {k} nokta: {k_closest(points, k)}")

# Uzaklıkları göster
print("\nTüm uzaklıklar:")
for x, y in points:
    dist = (x**2 + y**2) ** 0.5
    print(f"  ({x}, {y}) → {dist:.2f}")
Çıktı bekleniyor...
⏱️ Karmaşıklık: O(n log k) zaman, O(k) alan
5. Veri Akışında Medyan Orta

Sürekli gelen sayıların medyanını her an hesaplayın. (LeetCode #295)

Örnek:
addNum(1) → medyan: 1
addNum(2) → medyan: 1.5 (ortala)
addNum(3) → medyan: 2
💡 İpucu: İki heap kullan: max heap (küçük yarı), min heap (büyük yarı). Medyan = heap'lerin root'ları.

🧠 Düşünce Süreci

  1. small: Max heap (küçük yarı) - negatif değerlerle
  2. large: Min heap (büyük yarı)
  3. Her eleman eklendiğinde dengeyi koru
  4. Medyan: |small| > |large| ise small[0], değilse ortalama
Çözüm: Find Median from Data Stream
import heapq

class MedianFinder:
    def __init__(self):
        # small = max heap (negatif), large = min heap
        self.small = []  # Küçük yarı
        self.large = []  # Büyük yarı

    def addNum(self, num):
        # Önce small'a ekle (max heap = negatif)
        heapq.heappush(self.small, -num)

        # small'ın max'ı large'ın min'inden büyükse aktar
        if self.small and self.large and -self.small[0] > self.large[0]:
            val = -heapq.heappop(self.small)
            heapq.heappush(self.large, val)

        # Boyutları dengele (small en fazla 1 fazla olabilir)
        if len(self.small) > len(self.large) + 1:
            val = -heapq.heappop(self.small)
            heapq.heappush(self.large, val)

        if len(self.large) > len(self.small):
            val = heapq.heappop(self.large)
            heapq.heappush(self.small, -val)

    def findMedian(self):
        if len(self.small) > len(self.large):
            return -self.small[0]
        return (-self.small[0] + self.large[0]) / 2

# Test
mf = MedianFinder()
numbers = [5, 15, 1, 3, 2, 8, 7, 9]

print("Sayılar ekleniyor:")
for num in numbers:
    mf.addNum(num)
    median = mf.findMedian()
    print(f"  Ekle {num:2d} → Medyan: {median}")

print(f"\nSmall heap (küçük yarı): {[-x for x in mf.small]}")
print(f"Large heap (büyük yarı): {mf.large}")
Çıktı bekleniyor...
⏱️ Karmaşıklık:
addNum: O(log n)
findMedian: O(1)
Alan: O(n)
6. Task Scheduler Zor

Görevler dizisi ve n cooldown süresi verildiğinde, tüm görevleri tamamlamak için minimum süreyi bulun. Aynı görev arasında en az n birim beklenmeli. (LeetCode #621)

Örnek:
Input: tasks = ["A","A","A","B","B","B"], n = 2
Output: 8 → A → B → idle → A → B → idle → A → B

🧠 Düşünce Süreci

  1. En çok tekrarlanan görevi öncelikle yap → Max heap
  2. Cooldown'da olan görevleri queue'da tut
  3. Her adımda: heap'ten en çoklu görevi al, cooldown queue'ya ekle
Çözüm: Task Scheduler
import heapq
from collections import Counter, deque

def least_interval(tasks, n):
    """Minimum süreyi hesapla - O(m) m = toplam süre"""
    # Görev sayılarını hesapla
    count = Counter(tasks)

    # Max heap (negatif sayılar)
    max_heap = [-c for c in count.values()]
    heapq.heapify(max_heap)

    # Cooldown queue: (count, available_time)
    queue = deque()
    time = 0

    while max_heap or queue:
        time += 1

        if max_heap:
            cnt = 1 + heapq.heappop(max_heap)  # +1 çünkü negatif
            if cnt:  # Hala görevi varsa cooldown'a ekle
                queue.append((cnt, time + n))

        # Cooldown bitti mi kontrol et
        if queue and queue[0][1] == time:
            heapq.heappush(max_heap, queue.popleft()[0])

    return time

# Test
test_cases = [
    (["A","A","A","B","B","B"], 2),
    (["A","A","A","B","B","B"], 0),
    (["A","A","A","A","A","A","B","C","D","E","F","G"], 2),
]

for tasks, n in test_cases:
    result = least_interval(tasks, n)
    print(f"tasks={tasks}")
    print(f"n={n} → Minimum süre: {result}")
    print()
Çıktı bekleniyor...
⏱️ Karmaşıklık:
Zaman: O(m) - m toplam süre
Alan: O(26) = O(1) - en fazla 26 farklı görev
7. Son Stone Weight Kolay

Taş ağırlıkları verildiğinde, her turda en ağır iki taşı çarpıştır. Eşit ağırlıksa ikisi de yok olur, farklıysa fark kalır. Son taşın ağırlığını bulun. (LeetCode #1046)

Örnek:
Input: [2, 7, 4, 1, 8, 1]
→ 8 vs 7 = 1 kalır → [2, 4, 1, 1, 1]
→ 4 vs 2 = 2 kalır → [2, 1, 1, 1]
→ ...
Output: 1
Çözüm: Last Stone Weight
import heapq

def last_stone_weight(stones):
    """Son taşın ağırlığını bul - O(n log n)"""
    # Max heap (negatif değerler)
    max_heap = [-s for s in stones]
    heapq.heapify(max_heap)

    while len(max_heap) > 1:
        # En ağır iki taşı al
        stone1 = -heapq.heappop(max_heap)
        stone2 = -heapq.heappop(max_heap)

        print(f"  {stone1} vs {stone2}", end="")

        if stone1 != stone2:
            diff = stone1 - stone2
            heapq.heappush(max_heap, -diff)
            print(f" → {diff} kalır")
        else:
            print(" → ikisi de yok olur")

    return -max_heap[0] if max_heap else 0

# Test
stones = [2, 7, 4, 1, 8, 1]
print(f"Taşlar: {stones}\n")
print("Çarpışmalar:")
result = last_stone_weight(stones)
print(f"\n✅ Son taş ağırlığı: {result}")
Çıktı bekleniyor...
⏱️ Karmaşıklık: O(n log n) zaman, O(n) alan
8. Toplantı Odası II Orta

Toplantı aralıkları verildiğinde, minimum kaç oda gerektiğini bulun. (LeetCode #253)

Örnek:
Input: [[0,30], [5,10], [15,20]]
Output: 2 (0-30 ve 5-10 çakışıyor)

🧠 Düşünce Süreci

  1. Toplantıları başlangıç zamanına göre sırala
  2. Min heap ile aktif odaların bitiş zamanlarını tut
  3. Yeni toplantı için: en erken biten oda boşaldıysa onu kullan
Çözüm: Meeting Rooms II
import heapq

def min_meeting_rooms(intervals):
    """Minimum oda sayısını bul - O(n log n)"""
    if not intervals:
        return 0

    # Başlangıç zamanına göre sırala
    intervals.sort(key=lambda x: x[0])

    # Min heap: aktif odaların bitiş zamanları
    rooms = []

    for start, end in intervals:
        # En erken biten oda boşaldı mı?
        if rooms and rooms[0] <= start:
            heapq.heappop(rooms)  # Eski odayı boşalt

        # Bu toplantı için oda ayır
        heapq.heappush(rooms, end)

    return len(rooms)

# Test
test_cases = [
    [[0, 30], [5, 10], [15, 20]],  # 2 oda
    [[7, 10], [2, 4]],  # 1 oda
    [[0, 10], [5, 15], [10, 20]],  # 2 oda
]

for intervals in test_cases:
    result = min_meeting_rooms(intervals)
    print(f"Toplantılar: {intervals}")
    print(f"Minimum oda: {result}\n")
Çıktı bekleniyor...
⏱️ Karmaşıklık: O(n log n) zaman, O(n) alan

📋 Özet: Heap Ne Zaman Kullanılır?

✅ Heap Kullanım Senaryoları

Top-K Problems En büyük/küçük K eleman
Kth Element K. sıradaki eleman
Merge K Lists K sıralı yapıyı birleştir
Scheduling Task/meeting planlama
Stream Data Akan veride medyan/top-K