처음에는 그냥 전부 합쳐서 한 번에 정렬하면 끝이라고 생각했습니다. 그런데 다시 보니 각 리스트가 이미 정렬되어 있어서, 굳이 전체를 매번 다시 볼 필요는 없었습니다. 결국 이 문제는 단순 정렬 문제가 아니라 현재 후보들 중 가장 작은 값을 빠르게 꺼내는 문제였습니다.
문제
정렬된 리스트가 K개 주어집니다. 이를 하나로 합쳐서 정렬된 리스트를 반환하면 됩니다.
[
[10, 15, 30],
[12, 15, 20],
[17, 20, 32]
]
결과:
[10, 12, 15, 15, 17, 20, 20, 30, 32]
처음 떠오르는 풀이
def merge(lists):
arr = []
for lst in lists:
arr.extend(lst)
return sorted(arr)
구현도 쉽고 결과도 맞습니다. 하지만 각 리스트가 이미 정렬되어 있다는 조건을 전혀 활용하지 못합니다.
시간 복잡도는 O(KN log KN).
진짜 봐야 하는 점
매 순간 중요한 건 딱 하나입니다. 각 리스트의 맨 앞 후보들 중에서 가장 작은 값이 무엇인지 보는 것입니다.
처음 상태에서 비교할 값은 사실 세 개뿐입니다.
- 첫 번째 리스트: 10
- 두 번째 리스트: 12
- 세 번째 리스트: 17
10이 가장 작으니 먼저 꺼내면 됩니다. 그러면 첫 번째 리스트의 다음 값인 15가 새 후보가 됩니다.
- 첫 번째 리스트: 15 ← 방금 꺼낸 자리에 다음 값이 들어옴
- 두 번째 리스트: 12
- 세 번째 리스트: 17
이 과정을 반복하면 전체 정렬 결과를 얻을 수 있습니다. 이미 지나간 값보다 더 작은 값이 뒤에서 갑자기 튀어나올 일은 없기 때문입니다.
즉, 핵심은 현재 후보들 중 최솟값을 빠르게 꺼내는 것입니다. 이럴 때 잘 맞는 자료구조가 힙(heap)입니다.
왜 힙을 쓸까?
힙은 여러 값 중 가장 작은 값을 O(log K)에 꺼낼 수 있는 구조입니다.
힙에 넣을 정보는 이렇게 구성합니다.
(value, list_index, element_index)
예를 들어 (10, 0, 0)이면 0번 리스트의 0번 인덱스 값 10이라는 뜻입니다. 가장 작은 값을 꺼낸 뒤 어느 리스트에서 나왔는지 알 수 있으므로, 그 리스트의 다음 원소를 바로 힙에 넣을 수 있습니다.
코드
아래 코드는 위 흐름을 그대로 옮긴 구현입니다. 힙에는 각 리스트의 현재 원소만 넣고, 가장 작은 값을 꺼낼 때마다 해당 리스트의 다음 원소를 다시 넣습니다.
import heapq
def merge(lists):
merged = []
heap = []
for list_index, lst in enumerate(lists):
if lst:
heap.append((lst[0], list_index, 0))
heapq.heapify(heap)
while heap:
value, list_index, element_index = heapq.heappop(heap)
merged.append(value)
next_index = element_index + 1
if next_index < len(lists[list_index]):
next_value = lists[list_index][next_index]
heapq.heappush(heap, (next_value, list_index, next_index))
return merged
실행 예시
print(merge([[10, 15, 30], [12, 15, 20], [17, 20, 32]]))
# [10, 12, 15, 15, 17, 20, 20, 30, 32]
print(merge([])) # []
print(merge([[], [], []])) # []
print(merge([[], [1], [1, 2]])) # [1, 1, 2]
print(merge([[1], [1, 3, 5], [1, 10, 20, 30, 40]]))
# [1, 1, 1, 3, 5, 10, 20, 30, 40]
복잡도
전체 원소 수를 KN이라고 하면:
| 방식 | 시간 복잡도 |
|---|---|
| 단순 정렬 | O(KN log KN) |
| 힙 방식 | O(KN log K) |
리스트 개수 K가 많아질수록 힙 방식이 더 유리합니다. 보조 공간은 힙 크기 기준으로 O(K)입니다.
정리
- 쉬운 방법: 전부 합친 뒤 정렬 —
O(KN log KN) - 더 나은 방법: 힙으로 각 리스트의 현재 후보만 관리 —
O(KN log K) - 핵심: 정렬된 상태 활용, 최솟값 반복 추출, 우선순위 큐
짧아 보이는 문제지만, 설명하는 방식에 따라 면접에서 꽤 차이가 나는 유형입니다.
이 글은 코딩 면접 문제를 바탕으로, 풀이 아이디어를 제 방식대로 재구성해 정리한 글입니다.