8. Sort Colors
«Простота — это предварительное условие надежности».
— Эдсгер Дейкстра
LeetCode 75. Sort Colors
Сложность: Medium
Тема: Задача голландского национального флага (Dutch National Flag), три указателя, in-place сортировка за $O(N)$
Ссылки: Теория: Два указателя, Варианты применения, Remove Duplicates
1. Введение и физическая интуиция
Задача о голландском национальном флаге (Dutch National Flag problem) была сформулирована великим Эдсгером Дейкстрой в 1976 году. Цвета флага Нидерландов — красный, белый и синий. В задаче они закодированы числами 0 (красный), 1 (белый) и 2 (синий).
Требуется отсортировать массив из нулей, единиц и двоек за один проход ($O(N)$), используя строго $O(1)$ памяти и без вызова библиотечных сортировок.
Представьте детскую песочницу, где в ряд высыпаны разноцветные шарики. У нас есть три зоны:
- Слева — корзина для красных шариков (
0). - В центре — площадка для белых шариков (
1). - Справа — корзина для синих шариков (
2).
Мы ставим три указателя:
low: граница, куда должен лечь следующий найденный0.mid: наш текущий сканер (инспектор), обходящий шарики слева направо.high: граница, куда должен лечь следующий найденный2.
Инспектор mid берет очередной шарик:
- Видит
0? Бросает его влево на позициюlow, сдвигает обе границы:low++,mid++. - Видит
1? Оставляет на месте в центре:mid++. - Видит
2? Бросает его вправо на позициюhigh, уменьшаетhigh--. Но внимание: шарик, прилетевший с позицииhigh, инспектор еще не видел! Поэтомуmidостается на месте и инспектирует его на следующем шаге.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
A["nums = [2, 0, 2, 1, 1, 0]"]:::entry --> B["low = 0, mid = 0, high = len(nums)-1"]:::process
B --> C{"mid <= high?"}:::warning
C -->|"Да"| D{"nums[mid]"}:::warning
D -->|"0"| E["swap(low, mid)<br>low++, mid++"]:::process
D -->|"1"| F["mid++ (уже на месте)"]:::process
D -->|"2"| G["swap(mid, high)<br>high-- (mid не двигаем!)"]:::process
E --> C
F --> C
G --> C
C -->|"Нет"| H["Массив отсортирован in-place"]:::success
2. Формулировка задачи и вопросы интервьюеру
Дан массив nums, содержащий $n$ объектов цвета 0, 1 или 2. Отсортировать массив in-place так, чтобы одинаковые цвета шли рядом в строгом порядке 0, 1, 2. Запрещено использовать стандартную библиотечную функцию сортировки.
Вопросы интервьюеру (Senior-стиль):
- Каков размер массива?
«$1 \le len(nums) \le 300$. Задача решается за линейное время $O(N)$.» - Допустимо ли решение в два прохода (подсчет частот)?
«Counting sort (два прохода: сначала подсчитать число нулей, единиц и двоек, затем перезаписать массив) работает за $O(N)$. Однако на интервью уровня Senior всегда требуют однопроходный алгоритм (one-pass) с обменами на месте.» - Требуется ли стабильность сортировки?
«Нет, элементы представляют собой простые примитивыint.»
3. Инвариант трех указателей Дейкстры
Весь массив в любой момент выполнения делится на 4 четко разграниченные зоны:
+------------------+------------------+-------------------+------------------+
| Все нули (0) | Все единицы (1) | Еще не проверены | Все двойки (2) |
| [0 .. low-1] | [low .. mid-1] | [mid .. high] | [high+1 .. n-1] |
+------------------+------------------+-------------------+------------------+
^ ^ ^
low mid high
- До старта цикла:
low = 0,mid = 0,high = n - 1. Зоны нулей, единиц и двоек пусты. Весь массив лежит в зоне[mid .. high]. - В процессе работы: на каждом шаге зона неисследованных элементов
high - mid + 1уменьшается строго на 1. - По завершении: когда
mid > high, неисследованная зона схлопывается в 0. Массив идеально упорядочен.
4. Эталонная реализация на Go
package main
// SortColors сортирует срез nums, состоящий из 0, 1 и 2,
// за один проход по алгоритму Дейкстры (Dutch National Flag).
// Временная сложность: O(N) — каждый элемент обрабатывается не более 2 раз.
// Пространственная сложность: O(1) памяти — 0 аллокаций в куче.
func SortColors(nums []int) {
low := 0
mid := 0
high := len(nums) - 1
for mid <= high {
switch nums[mid] {
case 0:
// 0 отправляется в левую корзину.
// Так как слева от mid могут быть только единицы (или low == mid),
// на позицию mid попадает гарантированная 1, поэтому двигаем оба указателя.
nums[low], nums[mid] = nums[mid], nums[low]
low++
mid++
case 1:
// 1 уже находится в центральной зоне, просто шагаем дальше
mid++
case 2:
// 2 отправляется в правую корзину.
// На позицию mid прилетает элемент с конца, который мы еще НЕ инспектировали!
// Поэтому сдвигаем только high, а mid оставляем на месте.
nums[mid], nums[high] = nums[high], nums[mid]
high--
}
}
}
5. Пошаговая трассировка (Walkthrough)
Возьмем входной массив: nums = [2, 0, 2, 1, 1, 0].
| Шаг | low |
mid |
high |
nums[mid] |
Действие | Массив после шага |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 5 | 2 | swap(mid, high), high-- |
[0, 0, 2, 1, 1, 2] |
| 1 | 0 | 0 | 4 | 0 | swap(low, mid), low++, mid++ |
[0, 0, 2, 1, 1, 2] |
| 2 | 1 | 1 | 4 | 0 | swap(low, mid), low++, mid++ |
[0, 0, 2, 1, 1, 2] |
| 3 | 2 | 2 | 4 | 2 | swap(mid, high), high-- |
[0, 0, 1, 1, 2, 2] |
| 4 | 2 | 2 | 3 | 1 | mid++ |
[0, 0, 1, 1, 2, 2] |
| 5 | 2 | 3 | 3 | 1 | mid++ |
[0, 0, 1, 1, 2, 2] |
| 6 | 2 | 4 | 3 | — | mid > high $\implies$ Выход из цикла |
[0, 0, 1, 1, 2, 2] |
Всего 6 итераций для массива из 6 элементов. Ровно $O(N)$!
6. Mechanical Sympathy: почему множественное присваивание в Go идеально
В строках:
nums[low], nums[mid] = nums[mid], nums[low]
Go использует множественное присваивание (tuple assignment). Компилятор gc транслирует эту операцию не через выделение памяти в стеке, а через два процессорных регистра:
MOVQ (RAX), R8 ; R8 = nums[low]
MOVQ (RBX), R9 ; R9 = nums[mid]
MOVQ R9, (RAX) ; nums[low] = R9
MOVQ R8, (RBX) ; nums[mid] = R8
Обмен происходит ровно за 4 инструкции CPU без промежуточных переменных в памяти и без системных вызовов.
7. Вопросы для собеседования
8. Инварианты и чек-лист самопроверки
- [x] Условие цикла: строго
mid <= high(элемент на позицииhighдолжен быть проверен). - [x] При
case 0:low++иmid++. - [x] При
case 1: толькоmid++. - [x] При
case 2: толькоhigh--(midне меняется).
Следующая задача кластера демонстрирует встречные указатели с обратным заполнением результирующего среза: 9. Squares of Sorted Array.