Скользящее окно
Самая длинная подстрока без повторов
Найти длину самой длинной подстроки без повторяющихся символов
Постановка задачи
Описание (LeetCode - 3. Longest Substring Without Repeating Characters)
Дана строка s. Нужно найти длину самой длинной подстроки, в которой все
символы различны.
Пример 1:
s = "abcabcbb"
Результат:
3
Пояснение:
Самая длинная подстрока без повторов - "abc". Подстроки "bca" и "cab" тоже
подходят, их длина тоже равна 3.
Пример 2:
s = "bbbbb"
Результат:
1
Пример 3:
s = "pwwkew"
Результат:
3
Пояснение:
Самая длинная подстрока без повторов - "wke".
Ограничения:
0 <= s.length <= 10⁵sсостоит из английских букв, цифр, символов и пробелов
Решение
Наивное решение перебирает все подстроки и для каждой проверяет, есть ли в ней
повторы. Это занимает O(n²) или O(n³) и не подходит для строк длины 10⁵.
Нам нужна самая длинная непрерывная подстрока без повторов. Это классический случай динамического скользящего окна: правая граница расширяет окно, левая сдвигается только тогда, когда в окне появился дубликат.
Идея решения:
Поддерживаем окно [left..right] и множество seen символов внутри него.
Идем правой границей по строке. Если s[right] уже есть в seen, окно стало
недопустимым: в нем два одинаковых символа. Тогда сдвигаем left вправо и
убираем символы из множества, пока дубликат не исчезнет.
После этого добавляем s[right] в множество и обновляем максимальную длину
окна.
Пока в окне нет повторов, каждый новый символ справа увеличивает длину. Как только повтор появился, левая граница должна дойти как минимум до позиции сразу за предыдущим вхождением этого символа. Дальше влево возвращаться не нужно: любое более левое окно либо короче и все еще содержит повтор, либо уже было учтено раньше.
Основные шаги:
- Инициализируем множество
seen, указательleft = 0и переменнуюmaxLen = 0. - Идем указателем
rightпо строке. - Пока
s[right]уже есть вseen:- удаляем
s[left]из множества; - увеличиваем
left.
- удаляем
- Добавляем
s[right]вseen. - Обновляем
maxLen = max(maxLen, right - left + 1). - Возвращаем
maxLen.
Рассмотрим пример:
s = "abcabcbb"
right = 2: окноabc, все символы различны.maxLen = 3.
right = 3: добавляем вторуюa. В окне появился повтор, нужно сузить его слева.
- Убираем первую
aи сдвигаемleft. Окно становитсяbca, все символы снова различны.maxLenостается3.
- Продолжаем движение. На
right = 6в окнеabcbснова появляется повторb.
- Сдвигаем
left, пока втораяbне станет единственной. Окно становитсяcb. Максимум по-прежнему равен3.
Итог: ответ равен 3.
Оценка сложности
Временная сложность
Указатель right проходит по строке один раз. Указатель left только
сдвигается вперед и тоже проходит каждый символ не более одного раза. Добавление
и удаление из хеш-множества в среднем занимает O(1).
Итоговая временная сложность: O(n).
Пространственная сложность
В множестве хранятся символы текущего окна. В худшем случае все символы различны, и окно покрывает всю строку.
Пространственная сложность: O(n).
Код решения
Приведем код решения.
func lengthOfLongestSubstring(s string) int {
seen := map[byte]bool{}
left := 0
maxLen := 0
for right := 0; right < len(s); right++ {
for seen[s[right]] {
delete(seen, s[left])
left++
}
seen[s[right]] = true
maxLen = max(maxLen, right-left+1)
}
return maxLen
}Итоги
Задача Longest Substring Without Repeating Characters - базовый пример динамического скользящего окна на строках.
- Суть алгоритма: расширяем окно вправо и храним символы в множестве. Если
новый символ уже есть в окне, сдвигаем
left, пока повтор не исчезнет. - Эффективность:
- Временная сложность:
O(n), так как каждый символ входит в окно и выходит из него не более одного раза. - Пространственная сложность:
O(n)для множества символов текущего окна.
- Временная сложность:
- Граничные случаи:
- Пустая строка: ответ равен
0. - Все символы одинаковые: окно всегда длины
1. - Все символы различные: ответ равен длине всей строки.
- Пустая строка: ответ равен
- Оптимальное решение: мы не проверяем каждую подстроку с нуля, а поддерживаем самое длинное допустимое окно за один проход.