Pythonのheapqと最大ヒープのテクニック
heapq.heappush/heappopを使い、値を反転して最大ヒープを再現し、heapq.nlargest/nsmallestで上位k件をすばやく取得します。
「Pythonのheapqと最大ヒープのテクニック」はCoddyKit上の無料DSA Interview Prepレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはDSA Interview Prep学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 DSA Interview Prepコースには全4レッスンが含まれています。
Pythonのheapqモジュール概要
Pythonのheapqモジュールは、通常のPythonリスト上に実装された最小ヒープを提供します。専用のヒープクラスとは異なり、heapqは既存のリストをその場で操作します。モジュールが提供する関数は、O(n)でヒープを構築するheapify、O(log n)で要素を追加するheappush、O(log n)で最小要素を削除するheappop、そして複数の処理を効率よく組み合わせるheappushpop / heapreplaceです。
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])値の符号反転による最大ヒープ
Pythonのheapqが提供するのは最小ヒープだけです。最大ヒープをシミュレートするには、すべての値の符号を反転してからプッシュし、ポップするときにもう一度符号を反転します。これは、ヒープが格納された値に基づいて順序付けを行い、符号反転によって順序が逆になるためです。必ず両方の場面で符号を反転することを覚えておいてください。プッシュ前に反転し、ポップ後にも反転します。どちらか一方を忘れるのは、面接でよくあるバグです。
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とnsmallest
heapq.nlargest(k, iterable)とheapq.nsmallest(k, iterable)は、k個の最大要素または最小要素を返します。計算量はO(n log k)であり、kがnよりはるかに小さい場合は、すべてをソートする方法(O(n log n))より効率的です。内部では、サイズkのヒープを使用しています。kがnに近い場合、Pythonは完全なソートに切り替えます。永続的なヒープを維持せずに、1回限りのトップkクエリを処理したい場合に使用してください。
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]複雑なキーにタプルを使うヒープ
ヒープ要素にカスタム比較キーが必要な場合は、(priority, data)のようなタプルとして格納します。Pythonのheapqはタプルを要素ごとに比較するため、最初に優先度を比較します。優先度が等しい場合は2番目の要素を比較しますが、dataが比較できないとエラーになることがあります。最も安全なパターンは、一意のカウンターを同順位時の比較用に含め、data要素同士が直接比較されることを完全に避けることです。
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, P3heapq.merge:ソート済みイテラブルのマージ
heapq.merge(*iterables)は、複数のソート済みイテラブルを、すべてのデータをメモリに読み込むことなく、1つのソート済み出力へ遅延マージします。これはサイズkの最小ヒープを使ったk-wayマージと同等で、外部ソートアルゴリズムで使用されます。イテレータを返すため、要素は1つずつ生成されます。そのため、大規模なデータセットやストリーミング処理に適しています。
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))ヒープの遅延削除パターン
インデックスが分からないヒープから任意の要素を削除する必要がある場合は、遅延削除を使います。別の集合で要素に削除済みの印を付け、ポップ時にその要素をスキップします。これは償却O(log n)で、インデックスを追跡する複雑さを避けられます。重複したエントリを使うDijkstraのアルゴリズムや、タスクスケジューラのシミュレーションで標準的に使われる方法です。
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番目に大きい要素
ストリーム中のk番目に大きい要素(LeetCode #703)では、サイズkの最小ヒープを維持します。ヒープのルートは、これまでに見つかった要素の中で常にk番目に大きい要素になります。新しい数値が到着したら、それをプッシュし、ヒープのサイズがkを超えた場合は最小要素をポップします。ヒープ内には、それより大きい要素が正確にk-1個存在するため、ルートは常にk番目に大きい要素です。
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)和が最小となるk個のペアを見つける
和が最小となるk個のペアを見つける(LeetCode #373)では、最小ヒープを使ってペアを順番に生成します。まず、各jについて(nums1[0], nums2[j])をすべて追加します。最小のペアをポップしたら、取り出したペア(nums1[i], nums2[j])について、同じnums2の列にある次の候補(nums1[i+1], nums2[j])をプッシュします。これは、ヒープを使って順序付きのペアや積を生成する際によく使われるパターンです。
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]]最大ヒープを使ったタスクスケジューラ
Task Scheduler(LeetCode #621)では、同じタスクの間にn個の間隔のクールダウン期間を設けて、n個のタスクをスケジュールするための最小時間を求めます。タスクの出現回数について最大ヒープを使い、各時刻で実行可能なタスクのうち最も頻度の高いものを選び、その回数を1減らしてクールダウンに入れます。1サイクルにつきk=n+1個のタスクを処理するか、アイドル時間で埋めます。この最大ヒープを使った貪欲法により、最適な答えが得られます。
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)) # 6Dijkstraのアルゴリズムにおけるヒープ
Dijkstraのアルゴリズムにおける優先度付きキューは、最小ヒープで実装します。(distance, node)というタプルを格納し、未訪問のノードのうち最も近いものを常に先に処理します。ポップしたノードの距離が、現在分かっている最短経路より大きい場合、それは遅延削除による古いエントリなのでスキップします。これによりdecrease-key操作が不要になり、実装を単純に保ちながら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}最大ヒープを使った文字列の再構成
Reorganize String(LeetCode #767)では、隣り合う2つの文字が同じにならないように文字列を並べ替えます。(-frequency, char)の最大ヒープを使います。各ステップで、頻度が最も高い文字をポップします。直前の文字がその文字と同じ場合は、2番目に頻度の高い文字をポップします。この貪欲法により、最も制約の厳しい文字をできるだけ早く配置できます。
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)理解度チェック
このレッスンで扱ったData Structures & Algorithms — Coding Interview Prepの概念について、理解度を確認しましょう。
レッスンのまとめ
このレッスンでは、heapify、heappush、heappop、nlargest、nsmallest、mergeなどのPythonのheapqモジュールAPI、値の符号を反転して最大ヒープをシミュレートする方法、さらにトップkストリーミング、ストリーム中のk番目に大きい要素、タスクスケジューラ、Dijkstraなどのヒープに関する面接でよく使われるパターンを学びました。次はデータストリームの中央値とk-wayマージを扱います。
よくある質問
「Pythonのheapqと最大ヒープのテクニック」レッスンは無料ですか?
はい。「Pythonのheapqと最大ヒープのテクニック」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、DSA Interview Prepコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 DSA Interview Prepコースには全4レッスンが含まれています。
「Pythonのheapqと最大ヒープのテクニック」で何を学びますか?
heapq.heappush/heappopを使い、値を反転して最大ヒープを再現し、heapq.nlargest/nsmallestで上位k件をすばやく取得します。 ブラウザで直接実行するハンズオンコードでDSA Interview Prepを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
DSA Interview Prepを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのDSA Interview Prepは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「Pythonのheapqと最大ヒープのテクニック」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このDSA Interview Prepレッスンでコードを書いて実行できますか?
はい。すべてのDSA Interview Prepレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- ヒープの性質と配列表現
- heapify、push、popをゼロから実装する
- Pythonのheapqと最大ヒープのテクニック
- データストリームの中央値とK-wayマージ