6. Permutation in String
«Прежде чем бросаться решать задачу в общем виде, убедитесь, не задана ли граница заранее».
— Брайан Керниган
LeetCode 567. Permutation in String
Сложность: Medium
Тема: Фиксированное скользящее окно, частотный анализ, побайтовое сравнение массивов
Ссылки: Valid Anagram, Теория: Скользящее окно, Фиксированное и динамическое окно, Find All Anagrams
1. Введение и физическая интуиция
Перестановка строки s1 — это любая ее анаграмма: те же самые буквы в точно таком же количестве, но расположенные в произвольном порядке. Например, для строки s1 = "ab" перестановками являются "ab" и "ba".
Задача: выяснить, содержит ли длинная строка s2 подстроку, являющуюся перестановкой s1.
Представьте непрозрачный трафарет с вырезанным окошком длины ровно $K = len(s1)$.
Мы прикладываем этот трафарет к строке s2 и двигаем его слева направо:
- В каждый момент времени в окошке трафарета виден ровно срез длины $K$.
- Нам не важно, в каком порядке буквы стоят внутри окошка! Важен только частотный профиль: совпадает ли количество букв в окошке с частотным профилем шаблона
s1? - Сдвиг трафарета на 1 символ вправо — это добавление одного символа справа (
+1) и удаление одного символа слева (-1).
Поскольку длина шаблона фиксирована, размер окна ни на миллиметр не меняется на протяжении всей работы алгоритма.
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["s1 = 'ab', s2 = 'eidbaooo'"]:::entry --> B["target, window [26]int<br>Заполнить target по s1 и первое окно s2[0..1]"]:::process
B --> C{"window == target?"}:::warning
C -->|"Да"| D["Вернуть true"]:::success
C -->|"Нет"| E["i от len(s1) до len(s2)-1"]:::process
E --> F["window[s2[i]-'a']++ (вход справа)<br>window[s2[i-k]-'a']-- (выход слева)"]:::process
F --> G{"window == target?"}:::warning
G -->|"Да"| D
G -->|"Нет"| E
E --> H["Конец: вернуть false"]:::error
2. Формулировка задачи и вопросы интервьюеру
Даны две строки s1 и s2. Вернуть true, если s2 содержит перестановку s1, или false в противном случае. Иными словами, существует ли в s2 непрерывная подстрока, являющаяся анаграммой s1.
Вопросы интервьюеру (Senior-стиль):
- Каков алфавит входных строк?
«Строчные латинские буквы'a'..'z'. Это гарантирует, что частотный профиль умещается в массив[26]int.» - Что если
len(s1) > len(s2)?
«Перестановка более длинной строки не может содержаться в более короткой — сразу возвращаемfalseза $O(1)$.» - Требуется ли находить все вхождения или достаточно первого?
«Достаточно первого — при первом же совпадении делаем немедленный ранний выходreturn true.»
3. Разбор подходов
Подход 1: Сортировка каждого окна (Наивный)
Для каждого индекса $i$ брать срез s2[i : i+len(s1)], сортировать его байты и сравнивать с отсортированным s1.
- Сложность: $O(N \times K \log K)$ по времени, постоянные аллокации срезов в куче.
- Вердикт: Неприемлемо для интервью.
Подход 2: Фиксированное окно с частотными массивами [26]int (Эталонный)
- Сложность: строго $O(N)$ по времени.
- Память: $O(1)$ — два массива по 26 элементов
intна стеке (всего 416 байт). - Сравнение
window == targetв Go компилируется в одну оптимизированную операциюruntime.memequal.
4. Эталонная реализация на Go
package main
// CheckInclusion проверяет, содержит ли строка s2 перестановку строки s1.
// Временная сложность: O(len(s1) + len(s2)) — строго линейный обход.
// Пространственная сложность: O(1) памяти — 0 аллокаций в куче.
func CheckInclusion(s1 string, s2 string) bool {
n1 := len(s1)
n2 := len(s2)
// Перестановка s1 не может поместиться в строку меньшей длины
if n1 > n2 {
return false
}
var target [26]int
var window [26]int
// 1. Инициализируем эталонный профиль target и первое окно window
for i := 0; i < n1; i++ {
target[s1[i]-'a']++
window[s2[i]-'a']++
}
// Если первое же окно совпало с шаблоном — ранний выход
if window == target {
return true
}
// 2. Скользим окном длины n1 по строке s2
for i := n1; i < n2; i++ {
// Входящий символ справа
window[s2[i]-'a']++
// Выбывающий символ слева
window[s2[i-n1]-'a']--
// В Go массивы фиксированного размера являются value-типами!
// Сравнение window == target выполняется аппаратно через memequal за считанные такты.
if window == target {
return true
}
}
return false
}
5. Пошаговая трассировка (Walkthrough)
Вход: s1 = "ab", s2 = "eidbaooo" ($n1 = 2, n2 = 8$).
target = {'a': 1, 'b': 1}.
Окно i |
Подстрока | Выбыл | Вошел | window совпадает с target? |
Действие |
|---|---|---|---|---|---|
| Старт (0..1) | "ei" |
— | — | {'e':1, 'i':1} != target |
Идем дальше |
| 2 | "id" |
'e' |
'd' |
{'i':1, 'd':1} != target |
Идем дальше |
| 3 | "db" |
'i' |
'b' |
{'d':1, 'b':1} != target |
Идем дальше |
| 4 | "ba" |
'd' |
'a' |
{'b':1, 'a':1} == target |
Совпадение! return true |
Остаток строки "ooo" даже не читается в память. Ранний выход на 4-й итерации!
6. Mechanical Sympathy: почему window == target в Go работает мгновенно
Многие программисты избегают сравнения window == target внутри цикла, опасаясь скрытой сложности $O(26)$. Однако в компиляторе Go:
- Массив
[26]intзанимает 208 байт памяти (26 * 8). - Операция
==транслируется напрямую в вызов внутренней функцииruntime.memequal(p, q, 208)или разворачивается в несколько инструкций AVX-256 (VMOVDQU+VPCMPEQQ). - 208 байт сравниваются аппаратно всего за 2–3 такта процессора! Это в десятки раз быстрее, чем поддержка отдельного счетчика
matches == 26со сложными условными переходами, которые сбивают Branch Predictor CPU.
7. Вопросы для собеседования
8. Инварианты и чек-лист самопроверки
- [x] Проверка длин строк на старте:
if n1 > n2 { return false }. - [x] Формула выбывающего элемента:
s2[i - n1]. - [x] Zero Allocations: оба частотных массива живут строго на стеке.
Следующая задача кластера покажет, как динамическое окно с «ленивым» поддержанием максимума позволяет решать задачи с допустимыми заменами символов: 7. Longest Repeating Character Replacement.