Sort Colors
모든 값이 0, 1 또는 2인 배열 nums가 주어집니다. 이 값들을 세 가지 색, 예를 들어 빨강, 흰색, 파랑이라고 생각해 보세요. 배열의 모든 0이 먼저 오고, 그다음 모든 1이 오고, 마지막으로 모든 2가 오도록 재배열한 후 반환하세요.
라이브러리의 정렬 함수를 사용하지 않고 문제를 해결하세요. 핵심은 값에 대해 알고 있는 것을 활용하는 것입니다.
함수
- numsinteger-array
- 색상은 각각 0, 1 또는 2입니다
- 반환값integer-array
- 같은 값들을 먼저 모든 0, 그다음 모든 1, 그다음 모든 2 순서로
제약 조건
1 ≤ nums.length ≤ 1.5 × 104- 모든
nums[i]는0,1또는2입니다. - 색상이 누락될 수 있으며, 배열에는 단일 색상만 들어 있을 수 있습니다.
예제
- 입력
- nums = [2, 1, 0, 2, 0, 1, 1]
- 출력
- [0, 0, 1, 1, 1, 2, 2]
- 설명
- 배열에는 0이 두 개, 1이 세 개, 2가 두 개 들어 있으므로 결과도 정확히 같습니다. 0 두 개, 그다음 1 세 개, 그리고 2 두 개입니다.
- 입력
- nums = [2, 0, 2]
- 출력
- [0, 2, 2]
- 설명
- 1은 전혀 없습니다. 하나뿐인 0이 맨 앞으로 이동하고 두 개의 2가 그 뒤를 따릅니다.
- 입력
- nums = [1]
- 출력
- [1]
- 설명
- 단일 값은 이미 순서대로 정렬되어 있으므로 배열은 변경되지 않은 채 반환됩니다.
제출 시 숨은 테스트 +17개
후속 질문
배열의 길이보다 훨씬 작은 k개의 색상이 세 가지 대신 있다면 무엇을 바꾸겠습니까?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
서로 다른 값은 세 개만 나타날 수 있습니다. 일반적인 정렬로는 할 수 없지만, 이를 이용하면 무엇을 할 수 있을까요?
0, 1, 2의 개수를 세고 배열을 다시 쓰는 작업은 두 번의 순회로 이루어집니다. 한 번의 순회에서는 세 영역이 동시에 커지는 모습을 떠올려 보세요. 앞쪽에는 0, 뒤쪽에는 2, 그 사이에는 1이 있습니다.
인덱스 세 개를 유지하세요:
low,mid,high.nums[mid]를 읽으세요. 0이면low와 교환하고, 2이면high와 교환하며, 1이면 그대로 둡니다.high와 교환한 뒤에는 같은 위치를 다시 읽으세요.
풀이
어떤 정렬이든 올바른 순서를 만들므로, 핵심은 세 값이 어떤 작업을 건너뛸 수 있게 해 주느냐입니다. 0, 1, 2만 나타날 수 있으므로, 이 값들의 개수를 세고 두 번의 순회로 배열을 다시 쓸 수 있습니다. 0의 끝과 2의 시작 위치를 표시하는 세 개의 포인터를 사용하면 한 번의 순회만으로도 모든 값을 제자리에 놓을 수 있습니다. 이 한 번의 순회로 수행하는 분할이 네덜란드 국기 알고리즘입니다.
손으로 버블 정렬하기
정답이지만 가장 큰 테스트에서는 끝나지 않음
핵심 아이디어
라이브러리 정렬은 O(n log n)에 끝나지만, 문제에서는 이를 금지합니다. 면접관은 값이 세 종류뿐이라는 사실을 어떻게 활용하는지 보고 싶어 하기 때문입니다. 따라서 기본 방법은 직접 작성하는 정렬이며, 올바르게 구현하기 가장 간단한 방법은 버블 정렬입니다. 배열을 훑으면서 이웃한 두 값의 순서가 잘못되어 있으면 서로 바꿉니다.
한 번 훑을 때마다 마주치는 값 중 가장 큰 값이 거품처럼 위로 떠오르듯 배열의 맨 끝까지 이동합니다. 첫 번째 순회가 끝나면 마지막 위치가 확정되고, 두 번째 순회가 끝나면 마지막 두 위치가 확정됩니다. 따라서 n-1번 순회하면 배열 전체가 정렬됩니다. [2, 1, 0]에서는 첫 번째 순회로 2가 맨 끝으로 이동해 [1, 0, 2]가 되고, 두 번째 순회에서 1과 0의 위치를 바꿉니다.
각 순회에서 아직 위치가 확정되지 않은 모든 쌍을 비교하므로 느립니다. 비교 횟수는 전체적으로 약 n²/2회입니다. n = 1.5 × 10^4이면 비교 횟수가 10^8회를 넘고, 처음부터 순서가 잘못된 모든 쌍을 바꾸는 작업도 추가됩니다. 게다가 이 작업 중 어느 것도 값이 세 종류뿐이라는 사실을 활용하지 않습니다.
알고리즘
- 배열을 n-1회 순회합니다.
- 각 순회에서 아직 최종 위치가 정해지지 않은 모든 이웃 쌍
nums[j]와nums[j + 1]을 비교하고, 왼쪽 값이 더 크면 서로 바꿉니다. - 0부터 세는
done번째 순회가 끝나면 마지막done + 1개 위치에는 최종 값이 들어 있으므로, 다음 순회는 그 앞에서 멈춥니다. nums를 반환합니다.
def sortColors(nums):
n = len(nums)
for done in range(n - 1):
# One pass: the largest value left so far bubbles to index n-1-done.
for j in range(n - 1 - done):
if nums[j] > nums[j + 1]:
nums[j], nums[j + 1] = nums[j + 1], nums[j]
return nums각 색상의 개수를 센 다음 다시 작성하세요
핵심 아이디어
버블 정렬은 이웃한 값들을 비교하는 데 모든 시간을 쓰지만, 어떤 값들이 있는지는 이미 알고 있습니다. 배열에 0이 두 개, 1이 세 개, 2가 두 개 있다면, 아무것도 옮기기 전에 답은 정해져 있습니다. 0 두 개, 1 세 개, 2 두 개입니다. 개수만 중요합니다.
그러니 배열을 한 번 읽으며 각 값의 개수를 세세요. 그런 다음 처음부터 배열을 덮어쓰세요. count[0]개만큼 0을 쓰고, 이어서 count[1]개만큼 1을 쓰고, 그다음 count[2]개만큼 2를 씁니다. 이것이 계수 정렬이며, 같은 값끼리는 서로 바꾸어도 되므로 여기서는 안전합니다. 1은 그저 1이므로 원래 순서를 유지할 필요가 없습니다.
이 방법은 두 번 순회하고 카운터 세 개를 사용하므로 시간 복잡도는 O(n), 공간 복잡도는 O(1)입니다. 제약 조건을 충족하며, 색상이 여러 개일 때 자연스러운 해법입니다. 이 문제가 유명한 후속 질문은 배열을 한 번만 읽으면서도 이 작업을 할 수 있느냐는 것입니다.
알고리즘
- 세 개의 카운터를 만들고 모두 0으로 설정합니다.
- 모든 값을 읽고 해당 카운터를 1씩 증가시킵니다.
- 처음부터
count[0]개의 0을 쓰고, 그다음count[1]개의 1을 쓴 다음,count[2]개의 2를 씁니다. nums를 반환합니다.
def sortColors(nums):
count = [0, 0, 0] # how many 0s, 1s and 2s
for x in nums:
count[x] += 1
i = 0
for color in range(3):
for _ in range(count[color]):
nums[i] = color
i += 1
return nums세 개의 포인터를 사용한 한 번의 순회(네덜란드 국기 문제)
핵심 아이디어
읽으면서 세 구간을 만드세요. 앞쪽에는 0, 그 뒤에는 1, 뒤쪽에는 2를 두고, 1과 2 사이에는 아직 읽지 않은 부분을 둡니다. 세 개의 인덱스가 경계를 표시합니다. low 앞의 모든 값은 0이고, low부터 mid 미만까지는 모두 1이며, high 뒤의 모든 값은 2입니다. nums[mid]부터 nums[high]까지는 아직 읽지 않은 상태입니다.
nums[mid]를 읽습니다. 1은 이미 해당 구간에 있으므로 mid를 앞으로 이동합니다. 0은 앞쪽에 있어야 합니다. nums[low]와 교환하고 low와 mid를 모두 앞으로 이동합니다. low에서 돌아온 값은 1(또는 아직 1을 본 적이 없다면 같은 0)이므로 이미 제자리에 있습니다. 2는 뒤쪽에 있어야 합니다. nums[high]와 교환하고 high를 뒤로 이동하되, mid는 그대로 둡니다. high에서 온 값은 아직 읽지 않았기 때문입니다.
각 단계에서 mid는 앞으로 이동하거나 high는 뒤로 이동하므로, 읽지 않은 부분은 매번 한 칸씩 줄어들고 반복문은 n단계 뒤에 끝납니다. [2, 0, 2]를 추적해 봅시다. 첫 번째 2를 마지막 2와 교환하고 high를 1로 줄입니다. 인덱스 0에는 여전히 2가 있으므로 0과 교환하고 high를 0으로 줄입니다. 이제 인덱스 0에는 0이 있으므로 그대로 두면 [0, 2, 2]가 됩니다.
알고리즘
low = 0,mid = 0으로 설정하고high는 마지막 인덱스로 설정합니다.mid ≤ high인 동안nums[mid]를 읽습니다.- 값이 0이면
nums[low]와 교환하고low와mid를 오른쪽으로 한 칸 이동합니다. - 값이 1이면
mid를 오른쪽으로 한 칸 이동합니다. - 값이 2이면
nums[high]와 교환하고high를 왼쪽으로 한 칸 이동합니다.mid는 그대로 둡니다. nums를 반환합니다.
def sortColors(nums):
# nums[:low] are 0s, nums[low:mid] are 1s, nums[high + 1:] are 2s.
low, mid, high = 0, 0, len(nums) - 1
while mid <= high:
if nums[mid] == 0:
nums[low], nums[mid] = nums[mid], nums[low]
low += 1
mid += 1
elif nums[mid] == 1:
mid += 1
else:
# The value swapped in from high is unread, so mid stays.
nums[mid], nums[high] = nums[high], nums[mid]
high -= 1
return nums
함정과 경계 사례
한 번 순회하는 버전은 짧으며, 여기서 발생하는 거의 모든 버그는 움직이면 안 되는 포인터를 움직여서 생깁니다.
high와 교환한 뒤mid를 앞으로 이동하는 경우입니다. 도착한 값은 아직 읽지 않았습니다.[1, 2, 0]에서는 2가 0과 교환되며, 0을 건너뛰면[1, 0, 2]가 반환됩니다.high가 마지막으로 읽지 않은 인덱스일 때mid < high인 동안 반복하는 경우입니다. 두 포인터가 만났을 때 그 칸은 아직 읽지 않은 상태입니다.[1, 0]에서는 0을 읽기 전에 반복이 끝나고[1, 0]이 반환됩니다.- 부호 없는 인덱스에서
high가 0보다 작아지도록 두는 경우입니다.[2]처럼 2만 들어 있는 배열은high를 -1로 만듭니다. 인덱스가usize인 Rust에서는 Rust 코드처럼high를 읽지 않은 부분의 바로 다음 위치에 두세요. - 모든 색이 나타난다고 가정하는 경우입니다.
[2, 0, 2]에는 1이 없으며, 배열에 색 하나만 들어 있을 수도 있습니다. 포인터 규칙은 별도 처리 없이 두 경우를 모두 처리하므로, 별도의 예외 처리를 추가하지 마세요.
자주 묻는 질문4
네덜란드 국기 문제란 무엇인가요?
에츠허르 다익스트라가 제시한 문제입니다. 세 가지 색의 물체가 한 줄로 놓여 있을 때, 네덜란드 국기의 빨강, 흰색, 파랑을 한 번의 순회로 색깔별로 모으세요. 단, 교환만 사용할 수 있습니다. Sort Colors는 숫자 0, 1, 2를 사용하는 같은 문제입니다. 그의 해법은 low, mid, high를 사용하는 세 포인터 분할입니다.
Sort Colors의 시간 및 공간 복잡도는 어떻게 되나요?
한 번의 순회로 해결하는 방법은 읽지 않은 부분을 매 단계마다 셀 하나씩 줄이므로 O(n) 시간에 실행됩니다. 추가 공간은 O(1)만 사용합니다. 인덱스 세 개와 교환을 위한 임시 값이 필요합니다. 계수 정렬도 같은 시간 및 공간 복잡도를 가지지만 배열을 두 번 읽습니다.
high와 교환한 후에도 mid가 이동하지 않는 이유는 무엇인가요?
high에서 돌아온 값은 아직 읽은 적이 없으므로 0, 1 또는 2일 수 있습니다. mid를 그 값을 지나 이동시키면 가운데에 0이나 2가 남게 됩니다. low와 교환하는 것은 다릅니다. low와 mid 사이의 모든 값은 1이므로, 돌아오는 값은 알고 있는 값이며 mid를 계속 이동시킬 수 있습니다.
Sort Colors의 답으로 계수 정렬을 사용해도 괜찮을까요?
O(n) 시간 및 O(1) 공간 경계를 만족하며, 많은 면접관이 이를 첫 번째 답변으로 받아들입니다. 한 번의 순회로 해결하는 방법을 묻는 후속 질문에 대비하세요. 그 방법은 세 포인터 분할입니다. 색상이 많을 때는 계수 방식이 더 적합한데, 분할 방식은 세 그룹으로만 나누기 때문입니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def sortColors(nums):
# 여기에 코드를 작성하세요케이스 1
케이스 2
케이스 3
입력
nums = [2, 1, 0, 2, 0, 1, 1]
기대값
[0, 0, 1, 1, 1, 2, 2]