0Pricing
DSA Interview Prep · Lekcja

heapq w Pythonie i sztuczki z kopcem maksymalnym

Wykorzystają Państwo heapq.heappush/heappop, zanegują wartości, aby symulować kopiec maksymalny, oraz zastosują heapq.nlargest/nsmallest do szybkich zapytań top-k.

heapq w Pythonie i sztuczki z kopcem maksymalnym to bezpłatna lekcja DSA Interview Prep na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej DSA Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.

Przegląd modułu heapq języka Python

Moduł heapq języka Python udostępnia kopiec minimum zaimplementowany na zwykłej liście języka Python. W przeciwieństwie do dedykowanej klasy kopca moduł heapq działa bezpośrednio na istniejących listach. Funkcje modułu to: heapify do budowania kopca w czasie O(n), heappush do dodawania elementu w czasie O(log n), heappop do usuwania minimum w czasie O(log n) oraz heappushpop / heapreplace do wydajnego łączenia tych operacji.

import heapq

# heapq operates on plain Python lists
heap = []
heapq.heappush(heap, 5)
heapq.heappush(heap, 2)
heapq.heappush(heap, 8)
heapq.heappush(heap, 1)

print('Heap array:', heap)          # internal array (not sorted!)
print('Peek min:', heap[0])         # O(1) min access
print('Pop min:', heapq.heappop(heap))  # 1
print('Next min:', heap[0])         # 2

# heapify: turn any list into a heap in O(n)
data = [9, 4, 7, 1, 3, 6, 2]
heapq.heapify(data)
print('Heapified:', data, '| min:', data[0])

Kopiec maksymalny przez negowanie wartości

Pythonowy moduł heapq udostępnia tylko kopiec minimum. Aby zasymulować kopiec maksymalny, należy zanegować wszystkie wartości przed ich dodaniem, a następnie ponownie je zanegować podczas pobierania. Działa to dlatego, że kopiec porządkuje elementy według przechowywanych wartości, a negowanie odwraca kolejność. Należy zawsze pamiętać o negowaniu po obu stronach: negować przed dodaniem i po pobraniu. Pominięcie któregokolwiek z tych kroków jest częstym błędem podczas rozmów kwalifikacyjnych.

import heapq

max_heap = []
for val in [5, 1, 8, 3, 9, 2]:
    heapq.heappush(max_heap, -val)  # negate on push

print('Max-heap internal:', max_heap)  # all negated

# Pop in descending order:
results = []
while max_heap:
    results.append(-heapq.heappop(max_heap))  # negate on pop
print('Sorted descending:', results)  # [9, 8, 5, 3, 2, 1]

# Common pattern: top-k largest
data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
k = 3
heap = []
for x in data:
    heapq.heappush(heap, -x)
print('Top', k, ':', [-heapq.heappop(heap) for _ in range(k)])

heapq.nlargest i nsmallest

heapq.nlargest(k, iterable) i heapq.nsmallest(k, iterable) zwracają k największych lub najmniejszych elementów. Ich złożoność wynosi O(n log k), dzięki czemu są wydajniejsze od pełnego sortowania (O(n log n)), gdy k jest znacznie mniejsze od n. Wewnętrznie używają kopca o rozmiarze k. Gdy k jest zbliżone do n, Python przechodzi na pełne sortowanie. Należy używać tych funkcji do jednorazowych zapytań top-k bez utrzymywania trwałego kopca.

import heapq

data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 7]

# Top 3 largest:
print(heapq.nlargest(3, data))   # [9, 8, 7]
# Top 3 smallest:
print(heapq.nsmallest(3, data))  # [1, 1, 2]

# With a key function:
words = ['banana', 'apple', 'cherry', 'date', 'elderberry']
print(heapq.nlargest(2, words, key=len))   # ['elderberry', 'banana']
print(heapq.nsmallest(2, words, key=len))  # ['date', 'apple']

# Note: when k ~ n, use sorted() instead:
# sorted(data)[-k:]  or  sorted(data, reverse=True)[:k]

Kopiec z krotkami dla złożonych kluczy

Gdy elementy kopca wymagają własnego klucza porównywania, należy przechowywać je jako krotki (priority, data). Pythonowy moduł heapq porównuje krotki element po elemencie, więc najpierw porównuje priorytety. Jeśli priorytety są równe, porównuje drugi element — może to spowodować błędy, jeśli danych nie da się porównywać. Najbezpieczniejszy wzorzec polega na dodaniu unikalnego licznika jako elementu rozstrzygającego remisy, aby nigdy nie porównywać bezpośrednio elementów danych.

import heapq
import itertools

# Pattern: (priority, counter, item)
# Counter ensures unique tiebreaker, avoids comparing items
counter = itertools.count()
heap = []

def push_task(priority, task):
    heapq.heappush(heap, (priority, next(counter), task))

push_task(3, 'low priority task')
push_task(1, 'high priority task')
push_task(2, 'medium priority task')
push_task(1, 'another high priority')

while heap:
    pri, cnt, task = heapq.heappop(heap)
    print(f'P{pri}: {task}')
# Output in priority order: P1, P1, P2, P3

heapq.merge: scalanie posortowanych obiektów iterowalnych

heapq.merge(*iterables) leniwie scala wiele posortowanych obiektów iterowalnych w jeden posortowany wynik, bez wczytywania wszystkich danych do pamięci. Jest to odpowiednik scalania k-way z użyciem kopca minimum o rozmiarze k i znajduje zastosowanie w algorytmach sortowania zewnętrznego. Funkcja zwraca iterator, więc elementy są wytwarzane pojedynczo — idealnie sprawdza się w przypadku dużych zbiorów danych lub przetwarzania strumieniowego.

import heapq

# Merge multiple sorted lists efficiently
sorted_lists = [
    [1, 5, 9],
    [2, 6, 8],
    [3, 4, 7]
]

# heapq.merge takes sorted iterables and returns a merged sorted iterator
merged = list(heapq.merge(*sorted_lists))
print('Merged:', merged)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# The k-way merge manually (educational version):
def merge_k_sorted(lists):
    heap = []
    for i, lst in enumerate(lists):
        if lst:
            heapq.heappush(heap, (lst[0], i, 0))
    result = []
    while heap:
        val, list_idx, elem_idx = heapq.heappop(heap)
        result.append(val)
        if elem_idx + 1 < len(lists[list_idx]):
            heapq.heappush(heap, (lists[list_idx][elem_idx+1], list_idx, elem_idx+1))
    return result

print('Manual k-way:', merge_k_sorted(sorted_lists))

Wzorzec leniwego usuwania w kopcach

Gdy trzeba usuwać dowolne elementy z kopca, ale nie zna się ich indeksu, należy użyć leniwego usuwania: oznaczać elementy jako usunięte w osobnym zbiorze, a następnie pomijać je podczas pobierania. Zamortyzowana złożoność wynosi O(log n), a rozwiązanie pozwala uniknąć złożoności związanej ze śledzeniem indeksów. Jest to standardowe podejście w algorytmie Dijkstry z powielonymi wpisami oraz w symulacjach harmonogramów zadań.

import heapq

class LazyHeap:
    def __init__(self):
        self._heap = []
        self._removed = set()

    def push(self, task):
        heapq.heappush(self._heap, task)

    def remove(self, task):
        self._removed.add(task)  # mark as removed

    def pop(self):
        while self._heap:
            task = heapq.heappop(self._heap)
            if task not in self._removed:
                return task
        return None

lh = LazyHeap()
for t in [5, 1, 8, 3, 2]:
    lh.push(t)
lh.remove(1)  # 'delete' 1 lazily
lh.remove(8)  # 'delete' 8 lazily
results = [lh.pop() for _ in range(3)]
print(results)  # [2, 3, 5] -- 1 and 8 skipped

K-ty największy element w strumieniu

K-ty największy element w strumieniu (LeetCode #703) utrzymuje kopiec minimum o rozmiarze k. Korzeń kopca jest zawsze k-tym największym dotychczas napotkanym elementem. Gdy pojawia się nowa liczba, należy ją dodać, a jeśli rozmiar kopca przekroczy k, usunąć minimum. Korzeń jest zawsze k-tym największym elementem, ponieważ w kopcu znajduje się dokładnie k-1 elementów od niego większych.

import heapq

class KthLargest:
    def __init__(self, k, nums):
        self.k = k
        self.heap = []
        for num in nums:
            self.add(num)

    def add(self, val):
        heapq.heappush(self.heap, val)
        if len(self.heap) > self.k:
            heapq.heappop(self.heap)  # remove smallest
        return self.heap[0]  # kth largest = root of min-heap

# k=3, initial=[4,5,8,2]
kl = KthLargest(3, [4, 5, 8, 2])
print(kl.add(3))   # 4 (top 3: 8,5,4 -- kth=4)
print(kl.add(5))   # 5 (top 3: 8,5,5 -- kth=5)
print(kl.add(10))  # 5 (top 3: 10,8,5 -- kth=5)
print(kl.add(9))   # 8 (top 3: 10,9,8 -- kth=8)

Znajdowanie k par o najmniejszych sumach

Znajdowanie k par o najmniejszych sumach (LeetCode #373) wykorzystuje kopiec minimum do generowania par w odpowiedniej kolejności. Na początku należy dodać wszystkie pary (nums1[0], nums2[j]) dla każdego j. Następnie należy pobrać minimum, a dla pobranej pary (nums1[i], nums2[j]) dodać (nums1[i+1], nums2[j]) — następnego kandydata z tej samej kolumny nums2. Jest to częsty wzorzec generowania uporządkowanych par lub iloczynów za pomocą kopca.

import heapq

def k_smallest_pairs(nums1, nums2, k):
    if not nums1 or not nums2:
        return []
    heap = []
    # Initialize with pairs (nums1[0], nums2[j])
    for j in range(min(k, len(nums2))):
        heapq.heappush(heap, (nums1[0] + nums2[j], 0, j))
    result = []
    while heap and len(result) < k:
        total, i, j = heapq.heappop(heap)
        result.append([nums1[i], nums2[j]])
        if i + 1 < len(nums1):
            heapq.heappush(heap, (nums1[i+1] + nums2[j], i+1, j))
    return result

print(k_smallest_pairs([1,7,11], [2,4,6], 3))
# [[1,2], [1,4], [1,6]]

Harmonogram zadań z kopcem maksymalnym

Task Scheduler (LeetCode #621) wymaga znalezienia minimalnego czasu potrzebnego do zaplanowania n zadań z okresem chłodzenia wynoszącym n przedziałów między wystąpieniami tych samych zadań. Należy użyć kopca maksymalnego częstości zadań: w każdym kroku czasowym wybrać dostępne zadanie o największej częstości, zmniejszyć jego licznik i umieścić je w okresie chłodzenia. W każdym cyklu należy przetworzyć k=n+1 zadań (lub uzupełnić cykl czasem bezczynności). To zachłanne podejście z kopcem maksymalnym daje optymalne rozwiązanie.

import heapq
from collections import Counter

def least_interval(tasks, n):
    freq = Counter(tasks)
    heap = [-f for f in freq.values()]  # max-heap (negated)
    heapq.heapify(heap)
    time = 0
    while heap:
        cycle = n + 1
        temp = []
        for _ in range(cycle):
            if heap:
                temp.append(heapq.heappop(heap))
        for f in temp:
            if f + 1 < 0:  # still tasks remaining
                heapq.heappush(heap, f + 1)
        # Add full cycle or remaining tasks if queue empty
        time += cycle if heap else len(temp)
    return time

print(least_interval(['A','A','A','B','B','B'], 2))  # 8
print(least_interval(['A','A','A','B','B','B'], 0))  # 6

Kopiec w algorytmie Dijkstry

Kolejka priorytetowa w algorytmie Dijkstry jest implementowana za pomocą kopca minimum. Należy przechowywać krotki (distance, node) i zawsze najpierw przetwarzać najbliższy nieodwiedzony węzeł. Po pobraniu węzła, którego odległość jest większa od obecnie znanej najkrótszej ścieżki (jest to nieaktualny wpis wynikający z leniwego usuwania), należy go pominąć. Eliminuje to potrzebę operacji decrease-key i upraszcza implementację, przy zachowaniu złożoności O((V + E) log V).

import heapq

def dijkstra(graph, start):
    dist = {node: float('inf') for node in graph}
    dist[start] = 0
    heap = [(0, start)]  # (distance, node)
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:   # stale entry, skip
            continue
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(heap, (dist[v], v))
    return dist

graph = {
    'A': [('B', 4), ('C', 1)],
    'B': [('D', 1)],
    'C': [('B', 2), ('D', 5)],
    'D': []
}
print(dijkstra(graph, 'A'))  # {'A':0,'B':3,'C':1,'D':4}

Reorganizacja napisu za pomocą kopca maksymalnego

Reorganize String (LeetCode #767) wymaga przestawienia znaków w napisie tak, aby żadne dwa sąsiednie znaki nie były takie same. Należy użyć kopca maksymalnego z elementami (-frequency, char). W każdym kroku należy pobrać znak o największej częstości. Jeśli poprzedni znak jest taki sam jak znak o największej częstości, należy pobrać znak o drugiej co do wielkości częstości. To zachłanne podejście gwarantuje, że najbardziej ograniczany znak zostanie umieszczony tak wcześnie, jak to możliwe.

import heapq
from collections import Counter

def reorganize_string(s):
    freq = Counter(s)
    heap = [(-f, c) for c, f in freq.items()]
    heapq.heapify(heap)
    result = []
    prev_freq, prev_char = 0, ''
    while heap:
        freq, char = heapq.heappop(heap)
        result.append(char)
        # Push back the previous character if still remaining
        if prev_freq < 0:
            heapq.heappush(heap, (prev_freq, prev_char))
        prev_freq, prev_char = freq + 1, char  # decrement freq (less negative)
    result_str = ''.join(result)
    # Verify no adjacent duplicates
    return result_str if len(result_str) == len(s) else ''

print(reorganize_string('aab'))   # 'aba'
print(reorganize_string('aaab'))  # '' (impossible)

Szybkie sprawdzenie

Proszę sprawdzić swoje zrozumienie zagadnień Data Structures & Algorithms — Coding Interview Prep omówionych w tej lekcji.

Podsumowanie lekcji

W tej lekcji poznali Państwo: API modułu heapq języka Python, w tym funkcje heapify, heappush, heappop, nlargest, nsmallest i merge, symulowanie kopca maksymalnego przez negowanie wartości oraz częste wzorce zadań rekrutacyjnych dotyczące kopców, w tym przetwarzanie strumieniowe top-k, znajdowanie k-tego największego elementu w strumieniu, harmonogram zadań i algorytm Dijkstry. W następnej części zajmiemy się medianą ze strumienia danych oraz scalaniem k-way.

Często zadawane pytania

Czy lekcja „heapq w Pythonie i sztuczki z kopcem maksymalnym” jest bezpłatna?

Tak — pełny tekst „heapq w Pythonie i sztuczki z kopcem maksymalnym” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu DSA Interview Prep, przejdź na CoddyKit PRO. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.

Co nauczysz się w „heapq w Pythonie i sztuczki z kopcem maksymalnym”?

Wykorzystają Państwo heapq.heappush/heappop, zanegują wartości, aby symulować kopiec maksymalny, oraz zastosują heapq.nlargest/nsmallest do szybkich zapytań top-k. Ćwiczysz DSA Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć DSA Interview Prep?

Nie wymagamy żadnego doświadczenia. DSA Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „heapq w Pythonie i sztuczki z kopcem maksymalnym”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji DSA Interview Prep?

Tak. Każda lekcja DSA Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Własność kopca i reprezentacja tablicowa
  2. Heapify, push i pop od podstaw
  3. heapq w Pythonie i sztuczki z kopcem maksymalnym
  4. Mediana ze strumienia danych i scalanie k-way
← Powrót do DSA Interview Prep