|

우선순위 큐는 언제 써야 할까: 정렬로는 부족한 실시간 최솟값 문제 이해하기

우선순위 큐 글 대표 이미지
우선순위 큐와 힙이 필요한 상황을 정렬과 비교해 설명합니다. push, pop 비용, Python heapq, Java PriorityQueue 예시를 통해 Top K, 스케줄링, 다익스트라 문제에서 왜 쓰는지 정리합니다.

우선순위 큐는 값이 들어오고 나가는 중에도 가장 우선순위가 높은 값을 빠르게 꺼내기 위한 자료구조입니다. 정렬과 비슷해 보이지만, 핵심은 전체를 한 번 정렬하는 것이 아니라 매 순간 필요한 최솟값이나 최댓값을 꺼내는 것입니다.

힙은 우선순위 큐를 구현하는 대표 방식입니다. Python의 heapq와 Java의 PriorityQueue는 모두 이 흐름을 이해하면 훨씬 자연스럽게 쓸 수 있습니다.

우선순위 큐와 정렬 비교 카드
우선순위 큐는 값이 계속 바뀌는 상황에서 최솟값이나 최댓값을 빠르게 꺼내는 데 적합하다.

정렬로 충분한 상황

모든 데이터가 이미 주어져 있고, 한 번 정렬한 뒤 순서대로 쓰면 되는 문제라면 정렬이 단순합니다. 예를 들어 점수 목록을 내림차순으로 출력하는 문제는 정렬이 자연스럽습니다.

scores = [80, 95, 70, 100]
scores.sort(reverse=True)
print(scores[0])  # 가장 큰 점수

하지만 데이터가 계속 추가되고, 중간중간 가장 작은 값이나 큰 값을 꺼내야 한다면 매번 정렬하는 방식은 비효율적입니다.

우선순위 큐가 필요한 신호

  • 값이 계속 추가된다
  • 중간중간 최솟값 또는 최댓값을 꺼내야 한다
  • 전체 정렬 결과보다 현재 가장 우선순위 높은 값 하나가 중요하다
  • 스케줄링, Top K, 최단거리처럼 동적으로 후보가 바뀐다

이런 문제에서는 정렬된 전체 목록을 유지하려 하기보다 heap에 넣고 필요할 때 pop하는 흐름이 더 잘 맞습니다.

Python heapq 기본 사용법

Python heapq는 기본적으로 min heap입니다. 가장 작은 값이 먼저 나옵니다.

import heapq

heap = []
heapq.heappush(heap, 5)
heapq.heappush(heap, 2)
heapq.heappush(heap, 8)

print(heapq.heappop(heap))  # 2
print(heapq.heappop(heap))  # 5

최댓값이 필요하면 보통 값을 음수로 넣어 max heap처럼 씁니다.

heapq.heappush(heap, -score)
max_score = -heapq.heappop(heap)

Java PriorityQueue 기본 사용법

Java의 PriorityQueue도 기본은 작은 값이 먼저 나오는 구조입니다. 객체를 넣을 때는 Comparator로 우선순위를 정할 수 있습니다.

PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5);
pq.offer(2);
pq.offer(8);

System.out.println(pq.poll()); // 2
PriorityQueue<Integer> maxHeap =
    new PriorityQueue<>(Comparator.reverseOrder());

시간복잡도 감각

  • 삽입 push/offer: 보통 O(log n)
  • 삭제 pop/poll: 보통 O(log n)
  • 최솟값 확인 peek: 보통 O(1)
  • 전체 정렬: 보통 O(n log n)

값 하나가 들어올 때마다 전체를 다시 정렬하면 비용이 커집니다. 우선순위 큐는 필요한 위치만 조정해 최솟값 구조를 유지합니다.

Top K 문제에서 왜 유용할까

큰 데이터에서 상위 k개만 필요할 때 전체를 모두 정렬할 필요가 없습니다. 크기 k인 min heap을 유지하면 현재 top k 후보만 관리할 수 있습니다.

import heapq

def top_k(nums: list[int], k: int) -> list[int]:
    heap = []
    for num in nums:
        heapq.heappush(heap, num)
        if len(heap) > k:
            heapq.heappop(heap)
    return sorted(heap, reverse=True)

다익스트라에서의 우선순위 큐

다익스트라는 아직 확정되지 않은 정점 중 현재 거리가 가장 짧은 정점을 계속 꺼내야 합니다. 이 후보는 간선을 따라가며 계속 바뀌기 때문에 우선순위 큐가 잘 맞습니다.

heap = [(0, start)]

while heap:
    dist, node = heapq.heappop(heap)
    if dist > distance[node]:
        continue

    for next_node, cost in graph[node]:
        next_dist = dist + cost
        if next_dist < distance[next_node]:
            distance[next_node] = next_dist
            heapq.heappush(heap, (next_dist, next_node))

여기서 heap에는 같은 노드가 여러 번 들어갈 수 있습니다. 대신 꺼낼 때 이미 더 좋은 거리가 기록되어 있으면 건너뛰는 방식으로 처리합니다.

자주 하는 실수

  • PriorityQueue를 쓰면 내부 배열이 완전히 정렬되어 있다고 착각한다
  • Python heapq에서 최댓값이 필요한데 음수 변환을 빼먹는다
  • Java 객체 정렬 기준을 Comparator로 명확히 주지 않는다
  • heap 안의 특정 원소를 중간에서 삭제하려고 한다
  • 모든 값의 최종 정렬이 필요한 문제에 heap을 억지로 쓴다

정리

우선순위 큐는 전체를 정렬하기 위한 자료구조가 아니라, 계속 변하는 후보 중 가장 우선순위 높은 값을 빠르게 꺼내기 위한 도구입니다.

Python 구현은 heapq 문서, Java 구현은 PriorityQueue 문서에서 확인할 수 있습니다. 그래프 흐름은 그래프 입력 글과 함께 보면 좋습니다.

함께보면 좋은 글