Массивы: два указателя и скользящее окно
Задачи на массивы и строки — самые частые на собеседованиях. Наивное решение обычно перебирает пары за O(n²); приёмы этого урока делают то же за один проход O(n).
Два указателя навстречу
Указатели стоят на концах и двигаются к центру. Работает, когда массив отсортирован или задача симметрична (палиндромы, разворот).
def pair_sum_sorted(nums, target):
left, right = 0, len(nums) - 1
while left < right:
s = nums[left] + nums[right]
print(f" left={left} ({nums[left]}), right={right} ({nums[right]}), сумма={s}")
if s == target:
return left, right
if s < target:
left += 1 # сумма мала → нужен больший элемент слева
else:
right -= 1 # сумма велика → нужен меньший элемент справа
return None
print(pair_sum_sorted([1, 3, 4, 6, 8, 11], 10))
Пройдите пример 👣 Пошагово: видно, как указатели сближаются.
Почему это корректно: если nums[left] + nums[right] < target, то nums[left] не образует пару ни с каким элементом (все остальные справа не больше nums[right]) — его можно отбросить. Аналогично для правого.
Ещё задачи на этот шаблон: разворот массива на месте, проверка палиндрома, «контейнер с наибольшим количеством воды», сумма трёх чисел (3Sum: сортировка + два указателя для каждого первого элемента, O(n²)).
Быстрый и медленный указатели
Оба идут в одну сторону: fast просматривает элементы, slow указывает, куда записать следующий «хороший». Так массив меняют на месте, без дополнительной памяти.
def remove_duplicates(nums):
"""Удалить дубликаты из отсортированного списка на месте, вернуть новую длину."""
if not nums:
return 0
slow = 1
for fast in range(1, len(nums)):
if nums[fast] != nums[slow - 1]: # новый уникальный элемент
nums[slow] = nums[fast]
slow += 1
return slow
data = [1, 1, 2, 2, 2, 3, 5, 5]
k = remove_duplicates(data)
print(k, data[:k])
Этот же шаблон — «быстрый и медленный» — в связных списках находит середину и цикл (урок «Связные списки»).
Скользящее окно
Окно — непрерывный подотрезок [left, right]. При сдвиге окно обновляется, а не пересчитывается с нуля.
Фиксированный размер
def max_average(nums, k):
window = sum(nums[:k]) # первое окно — O(k)
best = window
for i in range(k, len(nums)):
window += nums[i] - nums[i - k] # вошёл новый, вышел старый — O(1)
best = max(best, window)
return best / k
print(max_average([1, 12, -5, -6, 50, 3], 4))
Переменный размер
Правый край расширяет окно, левый сжимает, когда условие нарушено:
left = 0
for right in range(len(s)):
добавить s[right] в окно
while окно невалидно:
убрать s[left] из окна
left += 1
обновить ответ по окну [left, right]
from collections import Counter
def min_window_with_sum(nums, target):
"""Длина кратчайшего подмассива с суммой >= target (числа положительные)."""
left = total = 0
best = float("inf")
for right, x in enumerate(nums):
total += x
while total >= target: # окно валидно — пробуем сжать
best = min(best, right - left + 1)
total -= nums[left]
left += 1
return 0 if best == float("inf") else best
print(min_window_with_sum([2, 3, 1, 2, 4, 3], 7)) # [4, 3] → 2
def longest_with_k_distinct(s, k):
"""Самая длинная подстрока, где не больше k разных символов."""
counts = Counter()
left = best = 0
for right, ch in enumerate(s):
counts[ch] += 1
while len(counts) > k:
counts[s[left]] -= 1
if counts[s[left]] == 0:
del counts[s[left]]
left += 1
best = max(best, right - left + 1)
return best
print(longest_with_k_distinct("eceba", 2)) # "ece" → 3
Каждый элемент входит в окно и выходит из него не более одного раза → O(n), хотя внутри цикла есть while.
Префиксные суммы
Если нужно много раз узнавать сумму отрезка, предвычислите префиксы: prefix[i] — сумма первых i элементов. Тогда сумма nums[l:r] = prefix[r] - prefix[l] за O(1):
from itertools import accumulate
nums = [3, -1, 4, 1, 5, 9, -2, 6]
prefix = [0] + list(accumulate(nums))
print(prefix)
def range_sum(l, r): # сумма nums[l:r]
return prefix[r] - prefix[l]
print(range_sum(2, 5), sum(nums[2:5]))
def count_subarrays_with_sum(nums, k):
"""Сколько подмассивов имеют сумму k (числа любого знака) — префиксы + словарь, O(n)."""
seen = {0: 1}
total = count = 0
for x in nums:
total += x
count += seen.get(total - k, 0) # есть ли префикс, дающий отрезок с суммой k
seen[total] = seen.get(total, 0) + 1
return count
print(count_subarrays_with_sum([1, 1, 1], 2), count_subarrays_with_sum([1, 2, 3, -3, 3], 3))
Как узнать шаблон
| Признак в условии | Приём |
|---|---|
| отсортированный массив, пара/тройка с суммой | два указателя навстречу |
| палиндром, разворот | два указателя навстречу |
| изменить массив на месте, удалить/переставить элементы | быстрый и медленный |
| «подмассив/подстрока подряд», «самая длинная/короткая», «не больше k» | скользящее окно |
| много запросов суммы отрезка, «подмассив с суммой k» | префиксные суммы (+ словарь) |
Окно не работает с отрицательными числами
Сжатие окна по сумме корректно, только когда числа положительные (сумма монотонна). Для «подмассив с суммой k» при отрицательных числах — префиксные суммы со словарём.
Итог
- Два указателя навстречу — отсортированные массивы и симметричные задачи.
- Быстрый/медленный — изменение массива на месте за O(1) памяти.
- Скользящее окно — подотрезки подряд; каждый элемент входит и выходит один раз → O(n).
- Префиксные суммы — сумма отрезка за O(1), с хеш-таблицей — подсчёт подмассивов с суммой k.
Практика решено 0 из 5
Пара с заданной суммой. Дан отсортированный по возрастанию список nums и число target.
Верните индексы (i, j), i < j, двух элементов с суммой target, или None, если таких нет.
Решение — двумя указателями за O(n) времени и O(1) памяти.
Какие тесты проверяют код (3)
assert pair_sum_sorted([1, 3, 4, 6, 8, 11], 10) == (2, 3) assert pair_sum_sorted([1, 2], 3) == (0, 1) and pair_sum_sorted([1, 2, 3], 100) is None assert pair_sum_sorted([], 0) is None
def pair_sum_sorted(nums, target):
left, right = 0, len(nums) - 1
while left < right:
s = nums[left] + nums[right]
if s == target:
return left, right
if s < target:
left += 1
else:
right -= 1
return None
Палиндром. Напишите is_palindrome(s): является ли строка палиндромом, если учитывать
только буквы и цифры и не учитывать регистр. "А роза упала на лапу Азора" → True.
Используйте два указателя, без создания очищенной копии строки (O(1) доп. памяти).
Какие тесты проверяют код (3)
assert is_palindrome("А роза упала на лапу Азора")
assert is_palindrome("A man, a plan, a canal: Panama") and not is_palindrome("race a car")
assert is_palindrome("") and is_palindrome(".,!")
def is_palindrome(s):
i, j = 0, len(s) - 1
while i < j:
if not s[i].isalnum():
i += 1
elif not s[j].isalnum():
j -= 1
elif s[i].lower() != s[j].lower():
return False
else:
i += 1
j -= 1
return True
Самая длинная подстрока без повторов. Верните длину самой длинной подстроки строки s,
в которой все символы различны. "abcabcbb" → 3 ("abc"). Скользящее окно, O(n).
Какие тесты проверяют код (4)
assert longest_unique("abcabcbb") == 3 and longest_unique("bbbbb") == 1
assert longest_unique("pwwkew") == 3 and longest_unique("") == 0
assert longest_unique("abba") == 2 and longest_unique("dvdf") == 3
import time
s = "abcdefghij" * 20_000
start = time.perf_counter()
assert longest_unique(s) == 10
assert time.perf_counter() - start < 1, "Нужно решение за O(n)"
def longest_unique(s):
last_seen = {}
left = best = 0
for right, ch in enumerate(s):
if ch in last_seen and last_seen[ch] >= left:
left = last_seen[ch] + 1
last_seen[ch] = right
best = max(best, right - left + 1)
return best
Перенос нулей. Переставьте все нули списка nums в конец на месте, сохранив порядок
остальных элементов. Функция move_zeroes(nums) ничего не возвращает. Быстрый и медленный указатели.
Какие тесты проверяют код (4)
a = [0, 1, 0, 3, 12]; assert move_zeroes(a) is None and a == [1, 3, 12, 0, 0] a = [0]; move_zeroes(a); assert a == [0] a = [4, 2, 0, 0, 1]; b = a; move_zeroes(a); assert b is a and a == [4, 2, 1, 0, 0] import inspect; src = inspect.getsource(move_zeroes); assert "sort" not in src, "Решите без сортировки — двумя указателями за O(n)"
def move_zeroes(nums):
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
Максимальная сумма окна. Верните наибольшую сумму k подряд идущих элементов списка
(гарантируется 1 ≤ k ≤ len(nums)). Окно фиксированного размера, O(n).
Какие тесты проверяют код (2)
assert max_window_sum([2, 1, 5, 1, 3, 2], 3) == 9 and max_window_sum([-1, -2, -3], 1) == -1 import time nums = list(range(60_000)) start = time.perf_counter() assert max_window_sum(nums, 3_000) == sum(range(57_000, 60_000)) assert time.perf_counter() - start < 0.25, "Пересчитывайте сумму окна за O(1) при сдвиге"
def max_window_sum(nums, k):
window = sum(nums[:k])
best = window
for i in range(k, len(nums)):
window += nums[i] - nums[i - k]
best = max(best, window)
return best
Войдите, чтобы отмечать пройденные уроки.