"우선순위 큐는 도착 순서를 무시하고 중요도로 손님을 받는 큐야. 그걸 만드는 방법이 힙이고, Python에서는 이미 만들어져 있어. heapq 모듈이지."
우선순위 큐 ADT
우선순위 큐는 연산이 둘뿐인 추상 자료형이야. 우선순위를 붙여 항목을 넣는 것, 그리고 우선순위가 가장 높은 항목을 꺼내는 것. 오래된 것부터 나가는 평범한 FIFO 큐와 달리 언제나 가장 중요한 원소를 다음에 건네줘. 첫 트랙에서 본 응급실 분류가 정확히 이거지. 힙이 그 정석 구현이고, 삽입 O(log n), 최고 원소 제거 O(log n), 최고 원소 엿보기 O(1)이야. 누가 '우선순위 큐'라고 하면 거의 언제나 '힙'을 뜻하는 거고.
Python의 heapq: 이미 다 만들어져 있어
sift-up이나 sift-down을 직접 구현할 일은 드물어. Python의 heapq가 평범한 리스트 위에서 heappush, heappop, heapify, heappushpop, nlargest와 nsmallest를 제공하거든. 전통적인 API는 최소 힙이고, Python 3.14부터는 heapify_max, heappush_max, heappop_max 같은 최대 힙 API도 들어왔어. 쓰는 버전을 확인하고 고르면 돼.
늘 필요한 두 관용구
- 최대 힙. Python 3.14 이상이면
heappush_max와heappop_max를 쓸 수 있어. 그보다 오래된 버전이거나 숫자만 다루는 호환 코드라면-value를 최소 힙에 넣는 관용구가 여전히 쓸모 있고. 상위 k개가 필요하면heapq.nlargest도 있어. - 튜플로 우선순위와 데이터를 함께.
(priority, item)을 push하면 힙이 우선순위 기준으로 정렬해 줘. 그런데 조심할 게 있어. 튜플은 원소를 앞에서부터 차례로 비교하거든. 그래서 우선순위가 같으면 Python이 item끼리 비교하려 들고, 비교가 안 되는 객체면 그대로 죽어 버려. 운 좋게 안 죽어도 의도하지 않은 순서가 나오고. 해결책은 계속 증가하는 카운터를 동점 처리용으로 끼워 넣는 거야.(priority, count, item)을 push하면count가 매번 다르니까 item이 비교되기 전에 모든 동점이 풀려.
우선순위 큐가 세상을 굴리는 곳
OS 작업 스케줄러가 우선순위가 가장 높은 프로세스를 돌리고, 이벤트 기반 시뮬레이션이 가장 빨리 일어날 이벤트를 처리하고, 대역폭과 QoS 조절이 돌아가고, A*와 Dijkstra 최단 경로 알고리즘이 다음 트랙에서 가장 가까운 경계 노드를 꺼내고, Huffman 코딩과 'top-k' 분석이 돌아가는 게 전부 우선순위 큐 위에서야. '뭔가가 스트림으로 들어오는데 언제나 가장 중요한 걸 다음에 처리해야 한다'는 구조인데, 실제 시스템에서 정말 흔하게 나오는 모양이야.
피파의 고백
TypeError: '<' not supported between instances of 'Task'를 뱉으며 죽었어. (priority, task) 튜플을 push했는데 작업 둘의 우선순위가 같아지자 Python이 동점을 깨려고 Task 객체를 비교하려 든 거야. 아빠가 한 줄로 고쳐줬어. 고유한 카운터를 끼워 넣어서 (priority, next(counter), task)로 만든 거지. 그러면 task를 건드리기 전에 동점이 반드시 풀려. 직접 부딪혀 봐야 배우는 종류의 함정이고, 지금은 heapq를 쓸 때 제일 먼저 챙기는 게 이거야.